CollectionView導致內存泄露?

本文將創建一個示例項目,運行後探查內存,發現本應被垃圾回收的UI控件沒有被回收。進一步發現是CollectionView導致控件不能被回收。最後,通過查看.NET Framework源代碼,發現其實不是內存泄露,虛驚一場。

發現問題

創建一個用戶控件GroupControl,有AddGroup(object header, object[] items)方法。這個方法就是創建一個GroupBox,設置Header和GroupBox裏面的ListBox.ItemsSource。

GroupControl.xaml

<ContentControl x:Class="Gqqnbig.TranscendentKill.GroupControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             mc:Ignorable="d" d:DesignHeight="300" d:DesignWidth="300">
    <ItemsControl Name="selectionGroupPanel" x:FieldModifier="private" HorizontalAlignment="Left" VerticalAlignment="Top"/>
</ContentControl>

GroupControl.xaml.cs

    public partial class GroupControl
    {
        public GroupControl()
        {
            InitializeComponent();
        }

        public event SelectionChangedEventHandler SelectionChanged;

        public void AddGroup(object header, object[] items)
        {
            GroupBox groupBox = new GroupBox();
            groupBox.Header = header;
            ListBox listBox = new ListBox();
            listBox.ItemsSource = items;
            listBox.SelectionChanged += listBox_SelectionChanged;
            groupBox.Content = listBox;

            selectionGroupPanel.Items.Add(groupBox);
        }

        void listBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (SelectionChanged != null)
                SelectionChanged(this, e);
        }
    }

然後主窗口使用這個GroupControl,在窗口加載的時候往GroupControl裏填數據,當用戶選擇GroupControl裏任意一項的時候,卸載這個GroupControl。

MainWindow.xaml

<Window x:Class="Gqqnbig.TranscendentKill.UI.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Width="800" x:ClassModifier="internal" Loaded="Window_Loaded_1">

</Window>

MainWindow.xaml.cs

    internal partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }


        private void Window_Loaded_1(object sender, RoutedEventArgs e)
        {
            Tuple<string, object[]>[] cps = new Tuple<string, object[]>[2];
            cps[0] = new Tuple<string, object[]>("時間", new[] { (object)DateTime.Now.ToShortTimeString() });
            cps[1] = new Tuple<string, object[]>("日期", new[] { (object)DateTime.Now.ToShortDateString() });


            GroupControl win = new GroupControl();

            for (int i = 0; i < cps.Length; i++)
            {
                ContentPresenter[] items = new ContentPresenter[cps[i].Item2.Length];

                for (int j = 0; j < cps[i].Item2.Length; j++)
                    items[j] = new ContentPresenter { Content = cps[i].Item2[j] };

                win.AddGroup(cps[i].Item1, items);
            }

            win.SelectionChanged += win_SelectionChanged;
            Content = win;
        }

        void win_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            GroupControl win = (GroupControl)this.Content;
            win.SelectionChanged -= win_SelectionChanged;

            Content = null;


            GC.Collect();
        }
    }


當卸載了GroupControl之後,儘管也調用了GC,我用.NET Memory Profiler查看,發現它還是存在。


圖1


圖2

圖2表示GroupBox._contextStorage保存了我的GroupControl;ListBox._parent保存了前面的GroupBox; ItemsPresenter保存了前面的ListBox;以此類推。因爲有對GroupControl的引用鏈存在,所以它無法被垃圾回收。

不徹底的解決方案

從引用鏈可以發現,ContentPresenter引用了父元素ListBoxItem,所以嘗試從ContentPresenter入手。不生成ContentPresenter,直接用原始的集合。

把MainWindow.cs的

            for (int i = 0; i < cps.Length; i++)
            {
                ContentPresenter[] items = new ContentPresenter[cps[i].Item2.Length];

                for (int j = 0; j < cps[i].Item2.Length; j++)
                    items[j] = new ContentPresenter { Content = cps[i].Item2[j] };

                win.AddGroup(cps[i].Item1, items);
            }
改爲

            for (int i = 0; i < cps.Length; i++)
            {
                //ContentPresenter[] items = new ContentPresenter[cps[i].Item2.Length];

                //for (int j = 0; j < cps[i].Item2.Length; j++)
                //    items[j] = new ContentPresenter { Content = cps[i].Item2[j] };

                win.AddGroup(cps[i].Item1, cps[i].Item2);
            }
。這樣探查內存,發現GroupControl消失了。問題疑似解決。


但這樣的解決方案留給人幾點疑惑:

  1. 控件之間的相互引用不應阻礙垃圾回收,只要它們沒有被外部的長生命週期的實例引用。
  2. 這個解決方案似乎不能得出什麼一般性的原則來避免疑似由ContentPresenter引起的內存泄露。衆所周知,WPF裏大量使用ContentPresenter,難道都會泄露?


仔細查看內存探查的結果(圖3),會發現ListCollectionView(也存在於圖2中的第7行)並沒有被垃圾回收。

圖3

所以,需要一個更徹底的解決方案。

尋找原因/徹底的解決方案

圖3說明ListCollectionView跟外部做了什麼交互,導致自己被引用上了,所以一連串都不能被回收。

我在VS裏輸入ListCollectionView,然後按F12轉到定義。我裝了Resharper,做過查看.net源代碼的配置,所以就可以轉到ListCollectionView的源代碼。可是不知爲什麼,ListCollectionView的代碼是空的。於是我轉到CollectionView。

然後查找哪裏使用了CollectionView。Reshaper 7.0疑似跟以前相比改進過了,可以查找.net類庫裏使用的類,於是我找到了ViewTable.Purge(),而且ViewTable也正好在引用鏈裏面。

圖4

查找ViewTable.Purge()的調用方,可以找到ViewManager.Purge()。繼續查找,定位到 DataBindEngine.GetViewRecord(object collection, CollectionViewSource key, Type collectionViewType, bool createView, Func<object, object> GetSourceItem):ViewRecord。所以差不多明白了,每當創建新的CollectionView的時候,就會調用Purge,就會刪除未使用的舊的CollectionView。

於是嘗試解決辦法,在MainWindow.cs的GC.Collect()之後,加上

            ListBox listBox = new ListBox();
            int[] n = { 1, 2, 3, 4 };
            listBox.ItemsSource = n;
            Content = listBox;

再探查內存,發現只有一個ListCollectionView,值爲1234;而舊的“時間”“日期”的CollectionView已經被銷燬了。

經過此番研究,結論是移除對ItemsControl的引用後,ItemsControl的CollectionView不會銷燬,ItemsControl本身可能也不會銷燬。如果再創建一個新的ItemsControl,並填充數據,舊的CollectionView和ItemsControl就會被銷燬了。




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章