引言
你可能知道,事件处理是内存泄漏的一个常见来源,它由不再使用的事件订阅对象存留产生,你也许认为它们已经被回收了,但并不是这样。
下面将用例子说明这个问题,之后会给出这个问题的标准解决方案:弱事件模式。有两种方法,即:
-
“传统”方法 (嗯,在 .Net 4.5 前,所以也没那么老),它实现起来比较繁琐
-
.Net 4.5 框架提供的新方法,它则是尽其可能的简单
(源代码在 这里 可供使用。)
从常见事物开始
在一头扎进本文核心内容前,让我们回顾一下在代码中最常使用的两个事物:类和方法。
事件源
下面是一个事件源类,包含事件的定义与发布:
public class EventSource
{
public event EventHandler Event = 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() 确保因执行终结方法而复活的对象也被清理了
引入问题
首先让我们试着通过一些理论,最重要的是还有一个演示的帮助,去了解事件监听器有哪些问题。
背景
一个对象要想被作为事件监听器,需要将该对象中的事件处理程序 注册(或挂载)到另一个能够产生事件的对象(即事件源)上,这样会导致事件源必须保持一个到事件侦听器对象的引用,以便在事件发生时调用此侦听器的处理方法。
这很合理,但如果这个引用是一个 强引用,则监听器会作为事件源的一个依赖 从而不能作为垃圾回收,即使引用它的对象是事件源。
例子
举例说明:
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()”的结果.
-
“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 为希望通过 WeakEvent 模式和 WeakEventManager 接收事件的类提供事件侦听支持
(这两个位于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>.
结论
在.Net上实现弱事件模式 是十分直接, 特别在 .Net 4.5.
如果你没有用.Net 4.5来实现,将需要一堆代码, 你可能不去用任何模式而是直接使用C# (+= and -=), 看看是否有内存问题,如果注意到泄露,还需要花必要的时间去实现一个。
但是用 .Net 4.5, 它是自由和简洁,而且由框架管理, 你可以毫无顾虑的选择它, 尽管没有 C# 语法 “+=” 和 “-=” 的酷, 但是语义是清晰的,这才是最重要的.
转自:开源中国 https://www.oschina.net/translate/the-net-weak-event-pattern-in-csharp?lang=chs&p=1
英文原文地址 https://www.codeproject.com/Articles/738109/The-Net-weak-event-pattern-in-Csharp
本文去掉了一些部分,在翻译上也稍有优化