使用 SharedResourceDictionary 时的内存泄漏

Posted

技术标签:

【中文标题】使用 SharedResourceDictionary 时的内存泄漏【英文标题】:Memory leak when using SharedResourceDictionary 【发布时间】:2011-10-15 00:01:58 【问题描述】:

如果您处理过一些较大的 wpf 应用程序,您可能熟悉 this。因为 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>

后面的Window代码是这样的

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 可以避免泄漏,但实际上并不总是这样。那么为什么 ResourceDictionary 会强引用 UserControls 呢? 我想知道为什么以前没有人经历过这种情况,就像我在一个较老的问题中所说的那样,似乎我们使用资源字典和 XAML 绝对是错误的,否则我想知道它们为什么如此低效。

我希望有人能对这件事有所了解。

提前致谢 妮可

【问题讨论】:

在您的内存分析器中,什么对象保留了对 MyUserControl 实例的引用? 我记不清了,但我几乎可以肯定它是一个 ResourceDictionary,在我的例子中是 SharedResourceDictionary。 【参考方案1】:

我不太确定这是否能解决您的问题。但是我在ResourceDictionary 引用控件时遇到了类似的问题,这与懒惰的hydration 有关。这是一个post。这段代码解决了我的问题:

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);
    

【讨论】:

感谢您提供的链接,这是寻找一些见解的良好开端。这已经帮助了我。虽然通过预先遍历所有资源的实际解决方案对我们来说并不完全是一个选择,因为我们有很多资源(有几个损坏的资源)。如果没有更好的结果,我会将您的答案标记为最佳答案。【参考方案2】:

我遇到了在大型 WPF 项目中需要共享资源目录的问题。阅读源文章和 cmets,我按照 cmets 中的建议对 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;
        
     

【讨论】:

这个方法的一个有趣的扩展使用了WeakReference,这样ResourceDictionary 对象可以在不使用时被垃圾回收。详情请见this blog post。 Drew,你有没有测试过垃圾收集是否有效? 对我来说,添加IsInDesignMode-check 不起作用。有什么建议吗? 效果很好,但我会在 URI 的构造函数中添加UriKind.RelativeOrAbsolute。结果将是没有多少异常被触发和捕获。

以上是关于使用 SharedResourceDictionary 时的内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

在使用加载数据流步骤的猪中,使用(使用 PigStorage)和不使用它有啥区别?

今目标使用教程 今目标任务使用篇

Qt静态编译时使用OpenSSL有三种方式(不使用,动态使用,静态使用,默认是动态使用)

MySQL db 在按日期排序时使用“使用位置;使用临时;使用文件排序”

使用“使用严格”作为“使用强”的备份

Kettle java脚本组件的使用说明(简单使用升级使用)