C#中的 .NET 弱事件模式
引言
你可能知道,事件处理是内存泄漏的一个常见来源,它由不再使用的对象存留产生,你也许认为它们应该已经被回收了,但不是,并有充分的理由。
在这个短文中(期望如此),我会在 .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.
弱事件模式
让我们看看在.NET中如何应付这个问题,
通常有超过一种方法去做,但是在这种情况下可以直接决定:
-
如果你正在使用 .Net 4.5 ,那么你将从简单的实现受益
-
另外,你必须依靠一点人为的技巧手段
传统方式
-
WeakEventManager 是所有模式管道的封装
-
IWeakEventListener 是管道,它允许一个组件连接到WeakEventManager管件
(这两个位于WindowBase程序集,你将需要参考你自己的如果你不在开发WPF项目,你应该准确的参考WindowBase)
因此这有两步处理.
首先通过继承WeakEventManager来实现一个自定义事件管理器:
-
重写 StartListening 和 StopListening 方法,分别注册一个新的handler和注销一个已存在的; 它们将被WeakEventManager基类使用。
-
提供两个方法来访问listener列表, 命名为 “AddListener” 和 “RemoveListener “,给自定义事件管理器的使用者使用。
-
通过在自定义事件管理器上暴露一个静态属性,提供一个方式去获得当前线程的事件管理器。
之后使listenr实现IWeakEventListenr接口:
-
实现 ReceiveWeakEvent 方法
-
尝试去处理这个事件
-
如果无误的处理好事件,将返回true
有很多要说的,但是可以相对地转换成一些代码:
首先是自定义弱事件管理器:
public class EventManager : WeakEventManager
{
private static EventManager CurrentManager
{
get
{
EventManager manager = (EventManager)GetCurrentManager(typeof(EventManager));
if (manager == null)
{
manager = new EventManager();
SetCurrentManager(typeof(EventManager), manager);
}
return manager;
}
}
public static void AddListener(EventSource source, IWeakEventListener listener)
{
CurrentManager.ProtectedAddListener(source, listener);
}
public static void RemoveListener(EventSource source, IWeakEventListener listener)
{
CurrentManager.ProtectedRemoveListener(source, listener);
}
protected override void StartListening(object source)
{
((EventSource)source).Event += DeliverEvent;
}
protected override void StopListening(object source)
{
((EventSource)source).Event -= DeliverEvent;
}
}
之后是事件listener:
public class LegacyWeakEventListener : IWeakEventListener
{
private void OnEvent(object source, EventArgs args)
{
Console.WriteLine("LegacyWeakEventListener received event.");
}
public LegacyWeakEventListener(EventSource source)
{
EventManager.AddListener(source, this);
}
public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
{
OnEvent(sender, e);
return true;
}
~LegacyWeakEventListener()
{
Console.WriteLine("LegacyWeakEventListener finalized.");
}
}
检查下:
Console.WriteLine("=== Legacy weak listener (better) ===");
EventSource source = new EventSource();
LegacyWeakEventListener listener = new LegacyWeakEventListener(source);
source.Raise();
Console.WriteLine("Setting listener to null.");
listener = null;
TriggerGC();
source.Raise();
Console.WriteLine("Setting source to null.");
source = null;
TriggerGC();
输出:
LegacyWeakEventListener received event.
Setting listener to null.
Starting GC.
LegacyWeakEventListener finalized.
GC finished.
Setting source to null.
Starting GC.
GC finished.
非常好,它起作用了,我们的事件listener对象现在可以在第一次GC里正确的析构,即使事件源对象还存活,不再泄露内存了.
但是要写一堆代码就为了一个简单的listener,想象一下你有一堆这样的listener,你必须要为每个类型的写一个弱事件管理器!
如果你很擅长代码重构,你可以发现一个聪明的方式去重构所有通用的代码.
在.Net 4.5 出现之前,你必须自己实现弱事件管理器,但是现在,.Net提供一个标准的解决方案来解决这个问题了,现在就来回顾下吧!
.Net 4.5 方式
.Net 4.5 已介绍了一个新的泛型版本的遗留WeakEventManager: WeakEventManager<TEventSource, TEventArgs>.
(这个类可以在WindowsBase集合.)
多亏了 .Net WeakEventManager<TEventSource, TEventArgs> 自己处理泛型, 不用去一个个实现新事件管理器.
而且代码还简单和可读:
public class WeakEventListener
{
private void OnEvent(object source, EventArgs args)
{
Console.WriteLine("WeakEventListener received event.");
}
public WeakEventListener(EventSource source)
{
WeakEventManager.AddHandler(source, "Event", OnEvent);
}
~WeakEventListener()
{
Console.WriteLine("WeakEventListener finalized.");
}
}
简单的一行代码,真简洁.
其他实现的使用也是相似的, 就是装入所有东西到事件listener类里:
Console.WriteLine("=== .Net 4.5 weak listener (best) ===");
EventSource source = new EventSource();
WeakEventListener listener = new WeakEventListener(source);
source.Raise();
Console.WriteLine("Setting listener to null.");
listener = null;
TriggerGC();
source.Raise();
Console.WriteLine("Setting source to null.");
source = null;
TriggerGC();
输出也是肯定正确的:
WeakEventListener received event.
Setting listener to null.
Starting GC.
WeakEventListener finalized.
GC finished.
Setting source to null.
Starting GC.
GC finished.
预期结果也跟之前一样,还有什么问题?!
结论
正如你看到的,在.Net上实现弱事件模式 是十分直接, 特别在 .Net 4.5.
如果你没有用.Net 4.5来实现,将需要一堆代码, 你可能不去用任何模式而是直接使用C# (+= and -=), 看看是否有内存问题,如果注意到泄露,还需要花必要的时间去实现一个。
但是用 .Net 4.5, 它是自由和简洁,而且由框架管理, 你可以毫无顾虑的选择它, 尽管没有 C# 语法 “+=” 和 “-=” 的酷, 但是语义是清晰的,这才是最重要的.