本文將創建一個示例項目,運行後探查內存,發現本應被垃圾回收的UI控件沒有被回收。進一步發現是CollectionView導致控件不能被回收。最後,通過查看.NET Framework源代碼,發現其實不是內存洩露,虛驚一場。
發現問題
創建一個用戶控件GroupControl,有AddGroup(object header, object[] items)方法。這個方法就是創建一個GroupBox,設置Header和GroupBox裡面的ListBox.ItemsSource。
GroupControl.xaml
[html]
<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
[csharp]
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
[html]
<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
[csharp]
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的
[csharp]
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);
}
改為
[csharp]
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消失了。問題疑似解決。
但這樣的解決方案留給人幾點疑惑:
控件之間的相互引用不應阻礙垃圾回收,只要它們沒有被外部的長生命周期的實例引用。
這個解決方案似乎不能得出什麼一般性的原則來避免疑似由ContentPresenter引起的內存洩露。眾所周知,WPF裡大量使用ContentPresenter,難道都會洩露?
仔細查看內存探查的結果(圖3),會發現ListCollectionView(也存在於圖2中的第7行)並沒有被垃圾回收。
圖3
所以,需要一個更徹底的解決方案。
尋找原因/徹底的解決方案
圖3說明ListCollectionView跟外部做了什麼交互,導致自己被引用上了,所以一連串都不能被回收。
我在VS裡輸入ListCollectionView,然後按F12轉到定義。我裝了Resharper,做過查看.net源代碼的配置,所以就可以轉到ListCollectionView的源代碼。可是不知為什麼,ListCollectionView的代碼是空的。於是我轉到CollectionView。 www.2cto.com
然後查找哪裡使用了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()之後,加上
[csharp]
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就會被銷毀了。