引言
你可能知道,事件处理是内存泄漏的一个常见来源,它由不再使用的对象存留产生,你也许认为它们应该已经被回收了,但不是,并有充分的理由。
在这个短文中(期望如此),我会在 .NET 框架的上下文事件处理中展示这个问题,之后我会教你这个问题的标准解决方案,弱事件模式。有两种方法,即:
-
“传统”方法 (嗯,在 .Net 4.5 前,所以也没那么老),它实现起来比较繁琐
-
.Net 4.5 框架提供的新方法,它则是尽其可能的简单
(源代码在 这里 可供使用。)
从常见事物开始
在一头扎进本文核心内容前,让我们回顾一下在代码中最常使用的两个事物:类和方法。
事件源
让我为您介绍一个基本但很有用的事件源类,它最低限度地揭示了足够的复杂性来说明这一点:
- public class EventSource
- {
- public event EventHandlerEvent = delegate { };
- public void Raise()
- {
- Event(this, EventArgs.Empty);
- }
- }
对好奇那个奇怪的空委托初始化方法(delegate { })的人来说,这是一个用来确保事件总被初始化的技巧,这样就可以不必每次在使用它之前都要检查它是否不为NULL。
触发垃圾收集的实用方法
在.net中,垃圾收集以一种不确定的方式触发。这对我们的实验很不利,我们的实验需要以一种确定的方式跟踪对象的状态。
所以,我们必须定期触发自己的垃圾收集操作,同时避免复制管道代码,管道代码已经在在一个特定的方法中释放:
- static void TriggerGC()
- {
- Console.WriteLine("Starting GC.");
- GC.Collect();
- GC.WaitForPendingFinalizers();
- GC.Collect();
- Console.WriteLine("GC finished.");
- }
虽然不是很复杂,但是如果你不是很熟悉这种模式,还是有必要小小解释一下:
-
第一个 GC.Collect() 触发.net的CLR垃圾收集器,对于负责清理不再使用的对象,和那些类中没有终结器(即C#中的析构函数)的对象,CLR垃圾收集器足够胜任
-
GC.WaitForPendingFinalizers() 等待其他对象的终结器执行;我们需要这样做,因为,你将看到我们使用终结器方法去追踪我们的对象在什么时候被收集的
-
第二个GC.Collect() 确保新生成的对象也被清理了
引入问题
首先让我们试着通过一些理论,最重要的是还有一个演示的帮助,去了解事件监听器有哪些问题。
背景
一个对象要想被作为事件侦听器,需要将其实例方法之一登记为另一个能够产生事件的对象(即事件源)的事件处理程序,事件源必须保持一个到事件侦听器对象的引用,以便在事件发生时调用此侦听器的处理方法。
这很合理,但如果这个引用是一个 强引用,则侦听器会作为事件源的一个依赖 从而不能作为垃圾回收,即使引用它的最后一个对象是事件源。
下面详细图解在这下面发生了什么:
事件处理问题
这将不是一个问题,如果你可以控制listener object的生命周期,你可以取消对事件源的订阅当当你不再需要listener,常常可以使用disposable pattern(用后就扔的模式)。
但是如果你不能在listener生命周期内验证单点响应,在确定性的方式中你不能把它处理掉,你必须依赖GC处理...这将从不会考虑你所准备的对象,只要事件源还存在着!
例子
理论都是好的,但还是让我们看看问题和真正的代码。
这是我们勇敢的时间监听器,还有点幼稚,我们很快知道为什么:
- public class NaiveEventListener
- {
- private void OnEvent(object source, EventArgs args)
- {
- Console.WriteLine("EventListener received event.");
- }
- public NaiveEventListener(EventSource source)
- {
- source.Event = OnEvent;
- }
- ~NaiveEventListener()
- {
- Console.WriteLine("NaiveEventListener finalized.");
- }
- }
用一个简单例子来看看怎么实现运作:
- Console.WriteLine("=== Naive listener (bad) ===");
- EventSource source = new EventSource();
- NaiveEventListener listener = new NaiveEventListener(source);
- source.Raise();
- Console.WriteLine("Setting listener to null.");
- listener = null;
- TriggerGC();
- source.Raise();
- Console.WriteLine("Setting source to null.");
- source = null;
- TriggerGC();
输出:
- EventListener received event.
- Setting listener to null.
- Starting GC.
- GC finished.
- EventListener received event.
- Setting source to null.
- Starting GC.
- NaiveEventListener finalized.
- GC finished.
让我们分析下这个运作流程:
-
“EventListener received event.“:这是我们调用 “source.Raise()”的结果; perfect, seems like we’re listening.
-
“Setting listener to null.“: 我们把本地事件监听器对象引用赋空值,这样应该可以让垃圾回收器回收了.
-
“Starting GC.“: 垃圾回收开始.
-
“GC finished.“: 垃圾回收开始, 但是 但是我们的事件监听器没有被回收器回收, 这样就证明了事件监听器的析构函数没有被调用。
-
“EventListener received event.“: 第二次调用 “source.Raise()”来确认,发现这监听器还活着。
-
“Setting source to null.“: 我们在赋空值给事件的原对象.
-
“Starting GC.“: 第二次垃圾回收.
-
“NaiveEventListener finalized.“: 这一次幼稚的事件监听终于被回收了,迟到总好过没有.
-
“GC finished.“:第二次垃圾回收完成.
结论:确实有一个隐藏的对事件监听器的强引用,目的是防止它在事件源被回收之前被回收!
希望有针对此问题的标准解决方案:让事件源可以通过弱引用来引用侦听器,在事件源存在时也可以回收侦听器对象。
这里有一个标准的模式及其在.NET框架上的实现:弱事件模式(http://msdn.microsoft.com/en-us/library/aa970850.aspx)。 And there is a standard pattern and its implementation in the .Net framework: the weak event pattern.