到目前为止,我的测试表明,在 silverlight 中利用 MVVM 模式的所有标准方法、示例和框架都存在一个巨大的问题:大量内存泄漏会阻止 VM 被垃圾收集。
显然这是一个巨大而荒谬的主张- 所以我的期望是有人会对我为什么以及哪里出错有一个明显的答案:)
重现的步骤很简单:
- 通过将视图数据上下文设置为 VM 将视图模型绑定到视图(假设视图模型利用 INotifyPropertyChanged 来支持数据绑定)
- 将 UI 元素绑定到视图模型上的属性,例如:
<TextBox Text="{Binding SomeText}" />
- 以某种方式利用绑定(例如 - 只需在文本框中输入)。
这将创建一个从根延伸到 BindingExpression 再到您的视图模型的引用链。然后,您可以从 UI 树中删除视图,以及对 VM 的所有引用 - 但是由于 root<>BindingExpression<>VM 引用链,VM 永远不会被垃圾收集。
我创建了两个示例来说明该问题。他们有一个按钮来创建一个新的视图/视图模型(它应该转储所有对旧视图的引用)和一个强制垃圾收集和报告当前内存使用情况的按钮。
示例 1 是一个超级精简的 caliburn 微示例。示例 2 没有使用任何框架,只是以我能想到的最简单的方式来说明问题。
对于那些可能希望提供帮助但不想下载示例项目的人,这里是示例 2 的代码。我们从一个名为 FooViewModel 的视图模型开始:
public class FooViewModel : INotifyPropertyChanged
{
string _fooText;
public string FooText
{
get { return _fooText; }
set
{
_fooText = value;
NotifyPropertyChanged("FooText");
}
}
private byte[] _data;
public FooViewModel()
{
_data = new byte[10485760]; //use up 10mb of memory
}
private void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
它只是公开了一个名为 FooText 的字符串属性,我们也将绑定它。INotifyPropertyChanged 是促进绑定所必需的。
然后我们有一个名为 FooView 的视图,它是一个用户控件,其中包含:
<UserControl x:Class="MVVMLeak.FooView">
<StackPanel x:Name="LayoutRoot" Orientation="Horizontal">
<TextBlock Text="Bound textbox: " />
<TextBox Text="{Binding FooText}" Width="100"/>
</StackPanel>
</UserControl>
(为简洁起见省略了命名空间)
这里重要的一点是绑定到 FooText 属性的文本框。当然,我们需要设置数据上下文,我选择在代码隐藏中执行此操作,而不是引入 ViewModelLocator:
public partial class FooView : UserControl
{
public FooView()
{
InitializeComponent();
this.DataContext = new FooViewModel();
}
}
主页看起来像这样:
<StackPanel x:Name="LayoutRoot" Background="White">
<Button Click="Button_Click" Content="Click for new FooView"/>
<Button Click="Button2_Click" Content="Click to garbage collect"/>
<ContentControl x:Name="myContent"></ContentControl>
</StackPanel>
在后面的代码中包含以下内容:
private void Button_Click(object sender, RoutedEventArgs e)
{
myContent.Content = new FooView();
}
private void Button2_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("Memory in use after collection: " + (GC.GetTotalMemory(true) / 1024 / 1024).ToString() + "MB");
}
注意:要复制问题,请务必在文本框中输入一些内容,因为我相信绑定表达式只有在需要时才会创建。
值得注意的是,这篇知识库文章可能是相关的,但我不相信,因为“方法 2”解决方法似乎没有效果,并且参考链似乎不匹配。
另外,我不确定这是否重要,但我使用CLR Profiler来诊断原因。
更新:
如果有人想测试并在评论中报告他们的发现,我将在此处通过保管箱托管 silverlight 应用程序:托管示例。重现:点击顶部按钮,输入内容,点击顶部按钮,输入内容,点击顶部按钮。然后按下按钮。如果它报告了 10MB 的使用量(或者可能是其他一些没有增加的量),那么您没有遇到内存泄漏。
到目前为止,问题似乎都发生在我们所有的开发机器上,这些机器是 ThinkPad w510 (43192RU),配备 12GB 内存,64 位 Win 7 Enterprise。这包括一些没有安装开发工具的。值得注意的是,他们正在运行 VMWare 工作站。
在我尝试过的其他机器上似乎没有出现这个问题——包括几台家用电脑和办公室里的其他电脑。我们在一定程度上排除了 SL 版本、内存量,可能还有 vmware。还没有确定原因。