gil*_*lyb 148 c# memory-leaks design-patterns event-handling
我刚刚意识到,通过阅读StackOverflow上的一些问题和答案,+=在C#(或者我猜,其他.net语言)中添加事件处理程序会导致常见的内存泄漏......
我过去曾多次使用这样的事件处理程序,并且从未意识到它们可能导致或导致我的应用程序中的内存泄漏.
这是如何工作的(意思是,为什么这实际上会导致内存泄漏)?
我该如何解决这个问题?是否-=足够使用相同的事件处理程序?
是否有常见的设计模式或最佳实践来处理这样的情况?
示例:我应该如何处理具有许多不同线程的应用程序,使用许多不同的事件处理程序在UI上引发多个事件?
是否有任何好的和简单的方法可以在已经构建的大型应用程序中有效地监控它?
Jon*_*eet 185
原因很简单:在订阅事件处理程序时,事件的发布者通过事件处理程序委托保存对订阅者的引用(假设委托是实例方法).
如果发布者的寿命比订阅者长,那么即使没有对订阅者的其他引用,它也会使订阅者保持活动状态.
如果您使用相同的处理程序取消订阅事件,那么是,这将删除处理程序和可能的泄漏.然而,根据我的经验,这实际上很少是一个问题 - 因为通常我发现发布者和订阅者的生命周期大致相等.
这是一个可能的原因......但根据我的经验,它过分夸大了.你的里程可能会有所不同,当然......你只需要小心.
Emr*_*ain 32
我在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 分析器中的 Force GC 按钮以确保调用垃圾收集器。然后,我拍摄了另一个内存快照并进行了比较。看!我们的恐惧是真的。即使在关闭后,垃圾收集器也不会收集子窗口。不仅如此,ChildWindow 对象的泄漏对象计数也显示为“ 3 ”(我单击按钮 3 次以显示 3 个子窗口)。
好的,然后,我分离了事件处理程序,如下所示。
然后,我执行了相同的步骤并检查了内存分析器。这一次,哇!没有更多的内存泄漏。