我已经在
https://www.spicelogic.com/Blog/net-event-handler-memory-leak-16的博客中解释了这个混乱。我将在此总结一下,以便您可以清楚地了解。
引用意味着“需要”:
首先,您需要了解的是,如果对象A持有对对象B的引用,那么这意味着对象A需要对象B才能运行,对吗?因此,只要对象A在内存中存在,垃圾收集器就不会收集对象B。
+=表示将右侧对象的引用注入到左侧对象中:
混淆来自C# +=运算符。该运算符并未明确告诉开发人员,该运算符的右侧实际上正在将引用注入到左侧对象中。
![enter image description here](https://istack.dev59.com/GLU0K.webp)
通过这样做,对象A认为它需要对象B,即使从您的角度来看,对象A不应该关心对象B是否存在。由于对象A认为需要对象B,只要对象A存活,就会保护对象B免受垃圾收集器的影响。但是,如果您不希望给事件订阅对象提供此保护,则可以说发生了内存泄漏。为了强调这一声明,在.NET世界中,没有像典型的C++非托管程序那样的内存泄漏概念。但是,正如我所说,对象A保护对象B免受垃圾回收的影响,如果这不是您的意图,则可以说发生了内存泄漏,因为对象B不应该存活在内存中。
![enter image description here](https://istack.dev59.com/x9tkL.webp)
您可以通过分离事件处理程序来避免此类泄漏。
如何做出决定?
您的整个代码库中有许多事件和事件处理程序。这是否意味着您需要在所有地方保持分离事件处理程序?答案是否定的。如果您必须这样做,那么您的代码库将变得非常丑陋和冗长。
您可以按照简单的流程图确定是否需要分离事件处理程序。
![enter image description here](https://istack.dev59.com/fkqrX.webp)
大多数情况下,您可能会发现事件订阅者对象与事件发布者对象一样重要,并且两者应该同时存在。
不需要担心的场景示例
例如,窗口的按钮单击事件。
![enter image description here](https://istack.dev59.com/UpXR0.webp)
在这里,事件发布者是按钮(Button),事件订阅者是MainWindow。应用该流程图,提出一个问题,Main Window(事件订阅者)是否应该在Button(事件发布者)之前死亡?显然不是。对吧?那甚至没有意义。那么,为什么要担心分离单击事件处理程序呢?
一个必须分离事件处理程序的例子。
我将提供一个示例,其中订阅对象在发布对象之前应该死亡。假设您的MainWindow通过单击按钮显示一个子窗口,并发布名为"SomethingHappened"的事件。子窗口订阅了主窗口的该事件。
![enter image description here](https://istack.dev59.com/TW7Cs.webp)
而且,子窗口订阅了主窗口的一个事件。
![enter image description here](https://istack.dev59.com/hxvlo.webp)
从这段代码中,我们可以清楚地了解到主窗口中有一个按钮。点击该按钮会显示一个子窗口。子窗口监听来自主窗口的事件。在做完某些事情后,用户关闭子窗口。
现在,根据我提供的流程图,如果你问一个问题:“事件订阅者(子窗口)是否应该在事件发布者(主窗口)之前死亡?”答案应该是是。所以,要分离事件处理程序。我通常会在窗口的Unloaded事件中这样做。
一个经验法则:如果你的视图(例如WPF、WinForm、UWP、Xamarin Form等)订阅了ViewModel的事件,一定要记得分离事件处理程序。因为ViewModel通常比视图存在时间更长。所以,如果ViewModel没有被销毁,任何订阅该ViewModel事件的视图都将留在内存中,这不好。
证明概念的方法是使用内存分析器。
如果我们不能用内存分析器验证概念,那就不会有太多乐趣。在这个实验中,我使用了JetBrain dotMemory profiler。
首先,我运行了MainWindow,它显示如下:
![enter image description here](https://istack.dev59.com/FTYuX.webp)
然后,我拍了一张内存快照。接着我点击了按钮
3次。三个子窗口出现了。我关闭了所有这些子窗口,并在dotMemory分析器中点击了Force GC按钮以确保垃圾回收器被调用。然后,我再次拍了一张内存快照并进行了比较。看哪!我们的担心成真了。即使这些子窗口已经关闭,Child Window仍未被垃圾回收器回收。不仅如此,ChildWindow对象的泄漏对象计数也显示为"
3"(我点击按钮3次以显示3个子窗口)。
![enter image description here](https://istack.dev59.com/bJPwz.webp)
好的,那么我将如下所示取消绑定事件处理程序。
![enter image description here](https://istack.dev59.com/42vpE.webp)
然后,我执行了相同的步骤并检查了内存分析器。这一次,哇!没有内存泄漏了。
![enter image description here](https://istack.dev59.com/fMD0n.webp)