在使用SharedResourceDictionary时出现内存泄漏问题

13
如果您曾经在一些较大的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>

现在添加一个用户控件。代码后台只是默认的,所以我只展示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代码如下:

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>

我知道这个程序并不完美,而且可能可以更简单,但是在找到展示问题的方法时,这就是我想出来的。总之:

开始运行程序并检查内存使用情况,如果你有一个内存分析器,这将变得更加容易。添加(通过点击选项卡显示)和删除页面,你会发现一切都很正常,没有泄漏。 现在,在 UserControl.Resources 部分中使用 SharedResourceDictionary 代替 ResourceDictionary 来包含 Shared.xaml。你会发现,在删除页面后,MyUserControl 仍然保存在内存中,以及其中的 MyUserControl

我想这对所有通过XAML实例化的东西都会发生,比如转换器、用户控件等等。奇怪的是,这对自定义控件不会发生。我猜这是因为自定义控件上没有真正实例化任何东西,数据模板等等。

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

我希望有人能够对这个问题有所启发。

提前感谢, Nico


在您的内存分析器中,是哪个对象保留了对MyUserControl实例的引用? - Luke Puplett
我记不太清了,但我几乎可以确定它是一个ResourceDictionary,对于我的情况来说是一个SharedResourceDictionary。 - dowhilefor
2个回答

12

我遇到了同样的问题,需要在一个相当大的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;
        }
    } 
}

3
这种方法的一个有趣扩展使用了“弱引用(WeakReference)”,这样当不使用“ResourceDictionary”对象时,它们可以被垃圾回收。详见此博客文章 - Drew Noakes
德鲁,你有没有测试过垃圾回收是否有效? - tofutim
1
对我来说,添加“IsInDesignMode”检查没有起作用。您有什么建议吗? - Kilian
它的运行非常完美,但我会在URI的构造函数中添加UriKind.RelativeOrAbsolute。结果将是不会触发和捕获那么多异常。 - Coden

7

我不太确定这是否能解决你的问题。但是,我曾经遇到过类似的问题,涉及到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);
    }
}

谢谢提供的链接,这是一个很好的开始寻找一些见解。这已经对我有所帮助了。虽然通过预遍历所有资源来找到实际解决方案对我们来说不是一个确切的选择,因为我们有很多资源(其中有一些是损坏的)。如果没有更好的解决方案,我将把您的答案标记为最佳答案。 - dowhilefor

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接