使用SharedResourceDictionary时内存泄漏

时间:2011-07-28 10:16:38

标签: wpf memory-leaks resourcedictionary

如果你在一些较大的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>

现在添加一个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>

后面的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可以避免泄漏,这实际上并不总是如此。那么为什么UserControls被ResourceDictionary强引用? 我想知道为什么以前没有人经历过这个,就像我在一个较旧的问题中说的那样,我们似乎使用资源词典和XAML绝对错误,否则我想知道为什么它们如此无能为力。

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

提前致谢 尼科

2 个答案:

答案 0 :(得分: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;
        }
    } 
}

答案 1 :(得分:7)

我不太确定这是否能解决您的问题。但是我对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);
    }
}