我在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 免受垃圾收集器的影响。但是,如果您不想为事件订阅者对象提供这种保护,那么可以说发生了内存泄漏。为了强调这一点,让我澄清一下,在 .NET 世界中,没有像典型的 C++ 非托管程序那样的内存泄漏概念。但是,正如我所说,对象 A 保护对象 B 免受垃圾回收,如果这不是您的意图,那么您可以说发生了内存泄漏,因为对象 B 不应该存在于内存中。
您可以通过分离事件处理程序来避免此类泄漏。
如何做决定?
您的整个代码库中有很多事件和事件处理程序。这是否意味着,您需要在任何地方保持分离事件处理程序?答案是否定的。如果你必须这样做,你的代码库会非常冗长而丑陋。
您可以按照一个简单的流程图来确定是否需要分离事件处理程序。
大多数时候,您可能会发现事件订阅者对象与事件发布者对象一样重要,并且两者都应该同时存在。
无需担心的场景示例
例如,窗口的按钮点击事件。
这里,事件发布者是Button,事件订阅者是MainWindow。应用该流程图,问一个问题,主窗口(事件订阅者)是否应该在按钮(事件发布者)之前死掉?显然没有。对吧?这甚至没有意义。那么,为什么还要担心分离点击事件处理程序呢?
事件处理程序分离是必须的示例。
我将提供一个示例,其中订阅者对象应该在发布者对象之前死亡。比如说,您的 MainWindow 发布了一个名为“SomethingHappened”的事件,并且您通过单击按钮从主窗口显示一个子窗口。子窗口订阅主窗口的那个事件。
而且,子窗口订阅了主窗口的一个事件。
从这段代码中,我们可以清楚的了解到主窗口中有一个按钮。单击该按钮会显示一个子窗口。子窗口监听来自主窗口的事件。做完某事后,用户关闭子窗口。
现在,根据我提供的流程图,如果您问一个问题“子窗口(事件订阅者)是否应该在事件发布者(主窗口)之前死亡?答案应该是肯定的。对吧?所以,分离事件处理程序。我通常从 Window 的 Unloaded 事件中执行此操作。
经验法则:如果您的视图(即 WPF、WinForm、UWP、Xamarin Form 等)订阅了 ViewModel 的事件,请始终记住分离事件处理程序。因为 ViewModel 通常比视图寿命更长。所以,如果 ViewModel 没有被销毁,那么订阅该 ViewModel 事件的任何视图都将留在内存中,这是不好的。
使用内存分析器证明概念。
如果我们不能使用内存分析器验证这个概念,那将不会很有趣。我在这个实验中使用了 JetBrain dotMemory profiler。
首先,我运行了 MainWindow,它显示如下:
然后,我拍了一张记忆快照。然后我点击了按钮 3 次。出现了三个子窗口。我已关闭所有这些子窗口并单击 dotMemory 分析器中的强制 GC 按钮以确保调用垃圾收集器。然后,我拍摄了另一个内存快照并进行了比较。看哪!我们的恐惧是真实的。即使在它们关闭之后,垃圾收集器也没有收集子窗口。不仅如此,ChildWindow 对象的泄漏对象计数也显示为“3”(我单击该按钮 3 次以显示 3 个子窗口)。
好的,那么,我分离了事件处理程序,如下所示。
然后,我执行了相同的步骤并检查了内存分析器。这一次,哇!不再有内存泄漏。