使用 SharedResourceDictionary 时发生内存泄漏

发布于 2024-11-26 18:32:28 字数 3652 浏览 1 评论 0原文

如果您开发过一些较大的 wpf 应用程序,您可能会熟悉。由于 ResourceDictionaries 始终被实例化,因此每次在 XAML 中找到它们时,我们最终可能会在内存中多次拥有一个资源字典。所以上面提到的解决方案似乎是一个非常好的选择。事实上对于我们当前的项目来说这个技巧做了很多...内存消耗从 800mb 下降到 44mb,这是一个非常巨大的影响。不幸的是,这个解决方案是有代价的,我想在这里展示这一点,并希望找到一种方法来避免它,同时仍然使用 SharedResourceDictionary

我做了一个小例子来可视化共享资源字典的问题。

只需创建一个简单的 WPF 应用程序即可。添加一项资源 Xaml

Shared.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <SolidColorBrush x:Key="myBrush" Color="Yellow"/>

</ResourceDictionary>

现在添加一个 UserControl。代码隐藏只是默认的,所以我只显示 xaml

MyUserControl.xaml

<UserControl x:Class="Leak.MyUserControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
             xmlns:SharedResourceDictionary="clr-namespace:Articy.SharedResourceDictionary" Height="128" Width="128">

    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="/Leak;component/Shared.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </UserControl.Resources>

    <Grid>
        <Rectangle Fill="{StaticResource myBrush}"/>     
    </Grid>
</UserControl>

后面的窗口代码看起来像这样

Window1.xaml.cs

// [ ... ]
    public Window1()
    {
        InitializeComponent();
        myTabs.ItemsSource = mItems;
    }

    private ObservableCollection<string> mItems = new ObservableCollection<string>();

    private void OnAdd(object aSender, RoutedEventArgs aE)
    {
        mItems.Add("Test");
    }
    private void OnRemove(object aSender, RoutedEventArgs aE)
    {
        mItems.RemoveAt(mItems.Count - 1);
    }

窗口 xaml 像这样

<窗口1.xaml

    <Window.Resources>
        <DataTemplate x:Key="myTemplate" DataType="{x:Type System:String}">
            <Leak:MyUserControl/>
        </DataTemplate>
    </Window.Resources>

    <Grid>
        <DockPanel>
            <StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
                <Button Content="Add" Click="OnAdd"/>
                <Button Content="Remove" Click="OnRemove"/>
            </StackPanel>
            <TabControl x:Name="myTabs" ContentTemplate="{StaticResource myTemplate}">
            </TabControl>
        </DockPanel>
    </Grid>
</Window>

我知道这个程序并不完美,可能可以变得更容易,但在找出一种方法来显示问题时,这就是我想出的。不管怎样:

启动这个并检查内存消耗,如果你有内存分析器,这会变得容易得多。添加(通过单击选项卡显示它)并删除页面,您将看到一切正常。没有任何泄漏。 现在,在 UserControl.Resources 部分中,使用 SharedResourceDictionary 而不是 ResourceDictionary 来包含 Shared.xaml。您将看到,在删除页面后,MyUserControl 将保留在内存中,并且其中的 MyUserControl 也将保留在内存中。

我认为这种情况会发生在通过 XAML 实例化的所有内容上,例如转换器、用户控件等。奇怪的是,这不会发生在自定义控件上。我的猜测是,因为自定义控件、数据模板等上没有真正实例化任何内容。

那么首先我们如何避免这种情况呢?在我们的例子中,必须使用 SharedResourceDictionary,但是内存泄漏使得无法有效地使用它。 使用 CustomControls 而不是 UserControls 可以避免泄漏,但这并不总是实际的。那么为什么 UserControls 被 ResourceDictionary 强引用呢? 我想知道为什么以前没有人经历过这个,就像我在一个旧问题中所说的那样,似乎我们使用资源字典和 XAML 绝对是错误的,否则我想知道为什么它们如此低效。

我希望有人能够对这个问题有所了解。

提前致谢 尼科

if you worked on some larger wpf applications you might be familiar with this. Because ResourceDictionaries are always instantiated, everytime they are found in an XAML we might end up having one resource dictionary multiple times in memory. So the above mentioned solution seems like a very good alternative. In fact for our current project this trick did a lot ... Memory consumption from 800mb down to 44mb, which is a really huge impact. Unfortunately this solution comes at a cost, which i would like to show here, and hopefully find a way to avoid it while still use the SharedResourceDictionary.

I made a small example to visualize the problem with a shared resource dictionary.

Just create a simple WPF Application. Add one resource Xaml

Shared.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

    <SolidColorBrush x:Key="myBrush" Color="Yellow"/>

</ResourceDictionary>

Now add a UserControl. The codebehind is just the default, so i just show the xaml

MyUserControl.xaml

<UserControl x:Class="Leak.MyUserControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
             xmlns:SharedResourceDictionary="clr-namespace:Articy.SharedResourceDictionary" Height="128" Width="128">

    <UserControl.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="/Leak;component/Shared.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </UserControl.Resources>

    <Grid>
        <Rectangle Fill="{StaticResource myBrush}"/>     
    </Grid>
</UserControl>

The Window code behind looks something like this

Window1.xaml.cs

// [ ... ]
    public Window1()
    {
        InitializeComponent();
        myTabs.ItemsSource = mItems;
    }

    private ObservableCollection<string> mItems = new ObservableCollection<string>();

    private void OnAdd(object aSender, RoutedEventArgs aE)
    {
        mItems.Add("Test");
    }
    private void OnRemove(object aSender, RoutedEventArgs aE)
    {
        mItems.RemoveAt(mItems.Count - 1);
    }

And the window xaml like this

Window1.xaml

    <Window.Resources>
        <DataTemplate x:Key="myTemplate" DataType="{x:Type System:String}">
            <Leak:MyUserControl/>
        </DataTemplate>
    </Window.Resources>

    <Grid>
        <DockPanel>
            <StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
                <Button Content="Add" Click="OnAdd"/>
                <Button Content="Remove" Click="OnRemove"/>
            </StackPanel>
            <TabControl x:Name="myTabs" ContentTemplate="{StaticResource myTemplate}">
            </TabControl>
        </DockPanel>
    </Grid>
</Window>

I know the program is not perfect and propably could be made easier but while figuring out a way to show the problem this is what i came up with. Anyway:

Start this and you check the memory consumption, if you have a memory profiler this becomes much easier. Add (with showing it by clicking on the tab) and remove a page and you will see everything works fine. Nothing leaks.
Now in the UserControl.Resources section use the SharedResourceDictionary instead of the ResourceDictionary to include the Shared.xaml. You will see that the MyUserControl will be kept in memory after you removed a page, and the MyUserControl in it.

I figured this happens to everything that is instantiated via XAML like converters, user controls etc. Strangely this won't happen to Custom controls. My guess is, because nothing is really instantiated on custom controls, data templates and so on.

So first how we can avoid that? In our case using SharedResourceDictionary is a must, but the memory leaks makes it impossible to use it productively.
The Leak can be avoided using CustomControls instead of UserControls, which is not always practically. So why are UserControls strong referenced by a ResourceDictionary?
I wonder why nobody experienced this before, like i said in an older question, it seems like we use resource dictionaries and XAML absolutely wrong, otherwise i wonder why they are so inefficent.

I hope somebody can shed some light on this matter.

Thanks in advance
Nico

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

枫林﹌晚霞¤ 2024-12-03 18:32:28

我遇到了同样的问题,在大型 WPF 项目中需要共享资源目录。阅读源文章和评论后,我按照评论中的建议对 SharedDirectory 类进行了一些修复,这似乎删除了强引用(存储在 _sourceUri 中),并且还使设计器能够正常工作。我测试了你的示例,它有效,在设计器和 MemProfiler 中都成功地注意到没有保留引用。我很想知道是否有人进一步改进了它,但这就是我现在要做的:

public class SharedResourceDictionary : ResourceDictionary
{
    /// <summary>
    /// Internal cache of loaded dictionaries 
    /// </summary>
    public static Dictionary<Uri, ResourceDictionary> _sharedDictionaries =
        new Dictionary<Uri, ResourceDictionary>();

    /// <summary>
    /// Local member of the source uri
    /// </summary>
    private Uri _sourceUri;

    /// <summary>
    /// Gets or sets the uniform resource identifier (URI) to load resources from.
    /// </summary>
    public new Uri Source
    {
        get {
            if (IsInDesignMode)
                return base.Source;
            return _sourceUri;
        }
        set
        {
            if (IsInDesignMode)
            {
                try
                {
                    _sourceUri = new Uri(value.OriginalString);
                }
                catch
                {
                    // do nothing?
                }

                return;
            }

            try
            {
                _sourceUri = new Uri(value.OriginalString);
            }
            catch
            {
                // do nothing?
            }

            if (!_sharedDictionaries.ContainsKey(value))
            {
                // If the dictionary is not yet loaded, load it by setting
                // the source of the base class

                base.Source = value;

                // add it to the cache
                _sharedDictionaries.Add(value, this);
            }
            else
            {
                // If the dictionary is already loaded, get it from the cache
                MergedDictionaries.Add(_sharedDictionaries[value]);
            }
        }
    }

    private static bool IsInDesignMode
    {
        get
        {
            return (bool)DependencyPropertyDescriptor.FromProperty(DesignerProperties.IsInDesignModeProperty,
            typeof(DependencyObject)).Metadata.DefaultValue;
        }
    } 
}

I'm running into the same issue of needing shared resource directories in a large-ish WPF project. Reading the source article and the comments, I incorporated a couple fixes to the SharedDirectory class as suggested in the comments, which seem to have removed the strong reference (stored in _sourceUri) and also make the designer work correctly. I tested your example and it works, both in the designer and MemProfiler successfully noting no held references. I'd love to know if anyone has improved it further, but this is what i'm going with for now:

public class SharedResourceDictionary : ResourceDictionary
{
    /// <summary>
    /// Internal cache of loaded dictionaries 
    /// </summary>
    public static Dictionary<Uri, ResourceDictionary> _sharedDictionaries =
        new Dictionary<Uri, ResourceDictionary>();

    /// <summary>
    /// Local member of the source uri
    /// </summary>
    private Uri _sourceUri;

    /// <summary>
    /// Gets or sets the uniform resource identifier (URI) to load resources from.
    /// </summary>
    public new Uri Source
    {
        get {
            if (IsInDesignMode)
                return base.Source;
            return _sourceUri;
        }
        set
        {
            if (IsInDesignMode)
            {
                try
                {
                    _sourceUri = new Uri(value.OriginalString);
                }
                catch
                {
                    // do nothing?
                }

                return;
            }

            try
            {
                _sourceUri = new Uri(value.OriginalString);
            }
            catch
            {
                // do nothing?
            }

            if (!_sharedDictionaries.ContainsKey(value))
            {
                // If the dictionary is not yet loaded, load it by setting
                // the source of the base class

                base.Source = value;

                // add it to the cache
                _sharedDictionaries.Add(value, this);
            }
            else
            {
                // If the dictionary is already loaded, get it from the cache
                MergedDictionaries.Add(_sharedDictionaries[value]);
            }
        }
    }

    private static bool IsInDesignMode
    {
        get
        {
            return (bool)DependencyPropertyDescriptor.FromProperty(DesignerProperties.IsInDesignModeProperty,
            typeof(DependencyObject)).Metadata.DefaultValue;
        }
    } 
}
白鸥掠海 2024-12-03 18:32:28

我不太确定这是否能解决您的问题。但我对 ResourceDictionary 引用控件及其与惰性 水合作用。这是帖子就可以了。这段代码解决了我的问题:

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        WalkDictionary(this.Resources);

        base.OnStartup(e);
    }

    private static void WalkDictionary(ResourceDictionary resources)
    {
        foreach (DictionaryEntry entry in resources)
        {
        }

        foreach (ResourceDictionary rd in resources.MergedDictionaries)
            WalkDictionary(rd);
    }
}

I am not quite sure if this will solve your issue. But I had similar issues with ResourceDictionary referencing controls and its to do with lazy hydration. Here is a post on it. And this code resolved my issues:

public partial class App : Application
{
    protected override void OnStartup(StartupEventArgs e)
    {
        WalkDictionary(this.Resources);

        base.OnStartup(e);
    }

    private static void WalkDictionary(ResourceDictionary resources)
    {
        foreach (DictionaryEntry entry in resources)
        {
        }

        foreach (ResourceDictionary rd in resources.MergedDictionaries)
            WalkDictionary(rd);
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文