为什么以及如何避免事件处理程序内存泄漏?

时间:2010-12-24 14:30:15

标签: c# design-patterns memory-leaks event-handling

我刚刚意识到,通过阅读StackOverflow上的一些问题和答案,在C#中使用+=添加事件处理程序(或者我猜,其他.net语言)会导致常见的内存泄漏...

我过去曾经多次使用这样的事件处理程序,并且从未意识到它们会导致或导致我的应用程序中的内存泄漏。

这是如何工作的(意思是,为什么这实际上会导致内存泄漏)? 我该如何解决这个问题?是否将-=足够用于同一个事件处理程序? 处理这种情况是否有共同的设计模式或最佳实践?
示例:我应该如何处理具有许多不同线程的应用程序,使用许多不同的事件处理程序在UI上引发多个事件?

是否有任何好的和简单的方法可以在已经构建的大型应用程序中有效地监控它?

5 个答案:

答案 0 :(得分:181)

原因很简单:在订阅事件处理程序时,事件的发布者通过事件处理程序委托保存对订阅者的引用(假设delegate是一种实例方法。)

如果发布者比订阅者的寿命更长,那么即使没有其他对订阅者的引用,它也会使订阅者保持活跃状态​​。

如果您使用相同的处理程序取消订阅事件,那么是,这将删除处理程序和可能的泄漏。然而,根据我的经验,这实际上很少是一个问题 - 因为通常我发现发布者和订阅者的生命周期大致相等。

是一个可能的原因......但根据我的经验,它过分炒作了。你的里程可能会有所不同,当然......你只需要小心。

答案 1 :(得分:21)

我在https://www.spicelogic.com/Blog/net-event-handler-memory-leak-16的博客中解释了这种混淆。我将在这里尝试对其进行总结,以便您有一个清晰的想法。

引用的意思是“需要”:

首先,您需要了解,如果对象A持有对对象B的引用,那么这意味着对象A需要对象B起作用,对吗?因此,只要对象A在内存中还活着,垃圾收集器就不会收集对象B。

我认为这对开发人员应该是显而易见的。

+ =意思是,将右侧对象的引用注入到左侧对象:

但是,混淆来自C#+ =运算符。该运算符未明确告知开发人员,该运算符的右侧实际上是在向左侧对象注入参考。

enter image description here

通过这样做,对象A认为它需要对象B,即使从您的角度来看,对象A不必关心对象B是否存在。当对象A认为需要对象B时,只要对象A仍然存在,对象A就会保护对象B免受垃圾回收器的攻击。但是,如果您不希望为事件订阅者对象提供这种保护,那么可以说发生了内存泄漏。

enter image description here

您可以通过分离事件处理程序来避免此类泄漏。

如何做出决定?

但是,整个代码库中有很多事件和事件处理程序。这是否意味着您需要在各处保持分离事件处理程序?答案是否定的。如果必须这样做,那么冗长的代码库将非常难看。

您可以按照简单的流程图来确定是否需要分离事件处理程序。

enter image description here

大多数时候,您可能会发现事件订阅者对象与事件发布者对象同等重要,并且都应该同时存在。

您无需担心的情况示例

例如,窗口的按钮单击事件。

enter image description here

在这里,事件发布者是Button,事件订阅者是MainWindow。应用该流程图,提出一个问题,主窗口(事件订阅者)是否应该在Button(事件发布者)之前失效?显然不是。那甚至没有意义。然后,为什么要担心分离单击事件处理程序?

必须分离事件处理程序的示例。

我将提供一个示例,其中订阅者对象应该在发布者对象之前死亡。假设您的MainWindow发布了一个名为“ SomethingHappened”的事件,并且您通过单击按钮从主窗口显示了一个子窗口。子窗口订阅了主窗口的事件。

enter image description here

然后,子窗口预订主窗口的事件。

enter image description here

通过此代码,我们可以清楚地了解主窗口中有一个按钮。单击该按钮将显示一个子窗口。子窗口侦听来自主窗口的事件。完成某项操作后,用户关闭子窗口。

现在,根据我提供的流程图,如果您问一个问题:“子窗口(事件订阅者)是否应该在事件发布者(主窗口)之前消失?答案应该是。对吗?事件处理程序。通常我是从Window的Unloaded事件中完成此操作的。

经验法则::如果您的视图(即WPF,WinForm,UWP,Xamarin Form等)订阅了ViewModel的事件,请务必记住分离事件处理程序。因为ViewModel通常比视图寿命更长。因此,如果未销毁ViewModel,则该ViewModel的订阅事件的任何视图都将保留在内存中,这是不好的。

使用内存分析器验证概念。

如果我们无法使用内存分析器验证该概念,那将不会很有趣。在此实验中,我使用了JetBrain dotMemory分析器。

首先,我运行了MainWindow,它显示如下:

enter image description here

然后,我拍摄了内存快照。然后,我点击了 3次按钮。出现了三个子窗口。我关闭了所有这些子窗口,然后单击dotMemory事件探查器中的“强制GC”按钮以确保调用了Garbage Collector。然后,我拍摄了另一个内存快照并进行了比较。看哪!我们的恐惧是真的。即使关闭了“垃圾收集器”,也未收集到“子窗口”。不仅如此,ChildWindow对象的泄漏对象计数还显示为“ 3 ”(我单击了3次按钮以显示3个子窗口)。

enter image description here

好吧,然后,我如下图所示分离了事件处理程序。

enter image description here

然后,我执行了相同的步骤并检查了内存分析器。这次,哇!没有更多的内存泄漏。

enter image description here

答案 2 :(得分:12)

是的,-=已经足够了,但是,跟踪每个分配的事件可能非常困难。 (详情见Jon的帖子)。关于设计模式,请查看weak event pattern

答案 3 :(得分:3)

事件实际上是事件处理程序的链接列表

如果对事件执行+ = new EventHandler,如果此特定函数之前已添加为侦听器并不重要,则每个+ =将添加一次。

当事件被引发时,它逐项浏览链接列表并调用添加到此列表中的所有方法(事件处理程序),这就是即使页面不再运行时仍然调用事件处理程序的原因只要他们活着(扎根),只要他们被联系起来,他们就会活着。所以他们会被调用,直到eventhandler被一个 - = new EventHandler取消挂起。

See Here

MSDN HERE

答案 4 :(得分:0)

我可以告诉您,这可能在Blazor中成为问题。您可以使用+=语法让Component订阅事件,从长远来看,这将导致泄漏。

对此(我知道)的唯一解决方案是不使用匿名方法,让Component从IDisposable继承,并使用Dispose()取消订阅事件处理程序。