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


154

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

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

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

在已经构建的大型应用程序中,是否有任何简便的方法可以有效地对此进行监视?

Answers:


188

原因很容易解释:订阅事件处理程序时,事件的发布通过事件处理程序委托来持有对订阅者的引用(假定委托是实例方法)。

如果发布者的寿命比订阅者的寿命长,那么即使没有其他对订阅者的引用,发布者的生命也将保持。

如果您使用相等的处理程序退订该事件,则可以,这将删除该处理程序和可能的泄漏。但是,以我的经验,这实际上很少是一个问题-因为通常我发现发布者和订阅者的生存期大致相同。

一个可能的原因...但是根据我的经验,它被大肆宣传。当然,您的里程可能会有所不同...您只需要小心。


...我已经看到一些人在回答诸如“ .net中最常见的内存泄漏是什么”之类的问题时回答问题。
gillyb 2010年

32
从发布者的角度解决此问题的一种方法是,一旦确定不再触发该事件,则将该事件设置为null。这将隐式删除所有订户,并且在某些事件仅在对象生存期的某些阶段触发时很有用。
JSBձոգչ2010年

2
Dipose方法将是将事件设置为null的好时机
Davi Fiamenghi 2015年

6
@DaviFiamenghi:好吧,如果有东西被处理掉,那至少表明它即将有资格进行垃圾收集,这时不管有什么订阅者。
乔恩·斯基特

是的,这是有道理的,我这样说是基于我与一个拥有许多定期订阅者的长期发行人的情况,在某个时刻我不得不重置/清除实例资源,但是对订阅者的引用仍然存在,所以我将其设置为nullon Dispose并调用它以清除那些“非托管”资源。也许更好的方法是调用方法ClearSubscriptionsClearData然后Dispose在我不再需要发布者之前在GC之前调用它?感谢您的答复
DAVI Fiamenghi


9

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

参考的意思是“需要”:

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

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

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

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

在此处输入图片说明

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

在此处输入图片说明

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

如何做出决定?

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

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

在此处输入图片说明

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

无需担心的方案示例

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

在此处输入图片说明

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

一个事件处理程序分离的例子是必须的。

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

在此处输入图片说明

并且,子窗口预订主窗口的事件。

在此处输入图片说明

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

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

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

使用内存分析器对概念进行证明。

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

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

在此处输入图片说明

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

在此处输入图片说明

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

在此处输入图片说明

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

在此处输入图片说明


3

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

当您对事件执行+ = new EventHandler时,此特定函数是否之前已添加为侦听器并不重要,它将按+ =一次添加一次。

引发事件时,将逐项浏览链接列表,并调用添加到此列表的所有方法(事件处理程序),这就是即使页面不再运行只要它们仍在运行时仍调用事件处理程序的原因活着(扎根),只要连接起来,它们就会活着。因此,它们将被调用,直到使用-= new EventHandler将事件处理程序解钩为止。

看这里

MSDN HERE


By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.