我已经在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个子窗口)。
好的,然后,我如下图所示分离了事件处理程序。
然后,我执行了相同的步骤并检查了内存分析器。这次,哇!没有更多的内存泄漏。