在 WPF PRISM/MVVM 应用程序中避免内存泄漏的最佳方法是啥

Posted

技术标签:

【中文标题】在 WPF PRISM/MVVM 应用程序中避免内存泄漏的最佳方法是啥【英文标题】:What's the best way to avoid memory leaks in WPF PRISM/MVVM application在 WPF PRISM/MVVM 应用程序中避免内存泄漏的最佳方法是什么 【发布时间】:2010-12-23 23:31:07 【问题描述】:

我有一个基于 PRISM 并利用 MVVM 模式的 WPF 应用程序。

我注意到,有时我的视图模型、视图以及与之相关的所有内容在其预期寿命之后仍会存在很长时间。

一个泄漏涉及在属于注入服务的集合上订阅 CollectionChanged,另一个涉及未调用 DispatcherTimer 上的 Stop 方法,还有一个需要清除集合中的项目。

我觉得使用 CompositePresentationEvent 可能比订阅 CollectionChanged 更可取,但在其他情况下,我倾向于实现 IDisposable 并让视图调用视图模型上的 Dispose 方法。

但是接下来需要告诉视图何时在视图模型上调用 Dispose,当视图的复杂性增加并且它们开始包含子视图时,这变得更没有吸引力了。

您认为处理视图模型以确保它们不会泄漏内存的最佳方法是什么?

提前致谢

伊恩

【问题讨论】:

【参考方案1】:

我可以告诉你,我已经经历了 100% 的痛苦。我想我们是内存泄漏兄弟。

不幸的是,我在这里想到的唯一事情与您的想法非常相似。

我们所做的是创建一个附加属性,视图可以将其应用于自身以将处理程序绑定到 ViewModel:

<UserControl ...
             common:LifecycleManagement.CloseHandler="Binding CloseAction">
...
</UserControl>

那么我们的 ViewModel 就只有一个 Action 类型的方法:

public MyVM : ViewModel

     public Action CloseAction
     
          get  return CloseActionInternal; 
     

     private void CloseActionInternal()
     
          //TODO: stop timers, cleanup, etc;
     

当我的 close 方法触发时(我们有几种方法可以做到这一点......它是一个 TabControl UI,在选项卡标题上带有“X”,诸如此类),我只是检查这个视图是否已经注册了自己与 AttachedProperty。如果是这样,我调用那里引用的方法。

这是一种非常迂回的方法,只是检查 View 的 DataContext 是否为 IDisposable,但当时感觉更好。检查 DataContext 的问题是您可能有也需要此控件的子视图模型。您要么必须确保您的视图模型链转发此 dispose 调用,要么检查图表中的所有视图并查看它们的数据上下文是否是 IDisposable(呃)。

我觉得这里少了点什么。还有一些其他框架试图以其他方式缓解这种情况。你可以看看Caliburn。它有一个处理这个问题的系统,其中 ViewModel 知道所有子视图模型,这使它能够自动向前链接事物。特别是,有一个名为 ISupportCustomShutdown 的接口(我认为这就是它的名称)有助于缓解此问题。

然而,我做过的最好的事情是确保并使用良好的内存泄漏工具,例如 Redgate Memory Profiler,它可以帮助您可视化对象图并找到根对象。如果您能够确定 DispatchTimer 问题,我想您已经在这样做了。

编辑:我忘记了一件重要的事情。 DelegateCommand 中的一个事件处理程序可能导致内存泄漏。这是 Codeplex 上关于它的一个线程,它解释了它。 http://compositewpf.codeplex.com/WorkItem/View.aspx?WorkItemId=4065

最新版本的 Prism (v2.1) 已修复此问题。 (http://www.microsoft.com/downloads/details.aspx?FamilyID=387c7a59-b217-4318-ad1b-cbc2ea453f40&displaylang=en)。

【讨论】:

大声笑,这是我一直在这里挖的一个深洞;)我很好奇你的附加行为与 UserControl 有什么关联,我一直在寻找一些方法来检测何时视图正在终止。卸载似乎是显而易见的选择,但它并没有太大帮助,因为它会在视图处于非活动状态时触发 啊这是习惯。每当视图需要关闭时,它都会调用我们拥有的一组应用程序命令的方法。 CloseView(对象视图)。我检查附加属性后面的商店,看看该视图或其任何子视图是否已注册为具有关闭的处理程序。如果是这样,我调用他们注册的方法。这里唯一的魔法是使用 LogicalTreeHelper 和 VisualTreeHelper 类来定位子视图。 msdn.microsoft.com/en-us/library/… 我们也尝试过卸载,顺便说一句。在某些情况下没关系......我们会停止基本上只是刷新数据的计时器,如果视图不可见(比如它在选项卡或其他东西上),则没有理由刷新它,但有时你只需要一个永不停止的计时器,除非视图被杀死,而这个明确的关闭是我们能想到的最好的方法。 @Anderson - 感谢您在 SO 上继续宣传 Prism。非常感谢您的回答!【参考方案2】:

到目前为止我的发现......

除了 PRISM、Unity、WPF 和 MVVM,我们还使用实体框架和 Xceed 数据网格。内存分析是使用 dotTrace 完成的。

我最终在我的视图模型的基类上实现了 IDisposable,其中 Dispose(bool) 方法被声明为虚拟,从而允许子类也有机会进行清理。 由于我们应用程序中的每个视图模型都从 Unity 中获取了一个子容器,因此我们也将其处理掉,在我们的例子中,这确保了 EF 的 ObjectContext 超出了范围。这是我们内存泄漏的主要来源。

视图模型放置在基本控制器类的显式 CloseView(UserControl) 方法中。它在视图的 DataContext 上查找 IDisposable 并在其上调用 Dispose。

Xceed 数据网格似乎导致了相当一部分的泄漏,尤其是在长时间运行的视图中。任何通过分配新集合来刷新数据网格的 ItemSource 的视图都应在分配新集合之前对现有集合调用 Clear()。

小心使用实体框架并避免任何长时间运行的对象上下文。当涉及到大型收藏时,这是非常无情的,即使您在打开跟踪的情况下删除了收藏,它也会引用收藏中的每个项目,即使您不再关注它们。

如果您不需要更新实体,请使用 MergeOption.NoTracking 检索它,尤其是在绑定到集合的长期视图中。

避免使用寿命长的视图,不要在它们不可见的区域内抓住它们,这会让你感到悲伤,尤其是当它们可见时定期刷新数据时。

当在 Xceed 列上使用 CellContentTemplates 时,不要使用动态资源,因为资源将持有对单元格的引用,从而使整个视图保持活动状态。

当在 Xceed 列上使用 CellEditor 并且资源存储在外部资源字典中时,将 x:Shared="False" 添加到包含 CellEditor 的资源中,资源将再次使用 x 保存对单元格的引用: Shared="False" 确保您每次都获得一份新副本,而旧副本会被正确删除。

在将 DelegateCommand 绑定到 Exceed 数据网格中的项目时要小心,如果在绑定到命令的行上有删除按钮等情况,请务必在关闭视图之前清除包含 ItemsSource 的集合。如果您正在刷新集合,您还需要重新初始化该命令,因为该命令将保存对每一行的引用。

【讨论】:

这提出了一个我应该提到的好点。上次发布的 Prism 版本在 DelegateCommand 中存在潜在的内存泄漏。它应该对它拥有的事件处理程序之一使用弱引用,但它没有。我最终分叉了代码并自己修复了它,但从那以后,他们已经修复了在 codeplex 上的存储库中签入的源代码。拉下最新的源码并编译,你也应该避免这个。

以上是关于在 WPF PRISM/MVVM 应用程序中避免内存泄漏的最佳方法是啥的主要内容,如果未能解决你的问题,请参考以下文章

WPF MVVM,Prism,Command Binding

Prism/MVVM (MEF/WPF):从模块中公开导航 [例如菜单]

Prism WPF AutoWireViewModel 异常

Prism框架的优点

Prism MVVM:“没有为类型定义无参数构造函数”将 ViewModel 绑定到 View 时出错

Prism - MVVM模式下,StackPanel中增加和删除View(UserControl)