你可能知道,事件处理是内存泄漏的一个常见来源,它由不再使用的对象存留产生,你也许认为它们应该已经被回收了,但不是,并有充分的理由。 在这个短文中(期望如此),我会在 .Net 框架的上下文事件处理中展示这个问题,之后我会教你这个问题的标准解决方案,弱事件模式。

引言

你可能知道,事件处理内存泄漏的一个常见来源,它由不再使用的对象存留产生,你也许认为它们应该已经被回收了,但不是,并有充分的理由。

在这个短文中(期望如此),我会在 .Net 框架的上下文事件处理中展示这个问题,之后我会教你这个问题的标准解决方案,弱事件模式。有两种方法,即:

  • “传统”方法 (嗯,在 .Net 4.5 前,所以也没那么老),它实现起来比较繁琐

  • .Net 4.5 框架提供的新方法,它则是尽其可能的简单

(源代码在 这里 可供使用。)

从常见事物开始

在一头扎进本文核心内容前,让我们回顾一下在代码中最常使用的两个事物:类和方法。

事件源

让我为您介绍一个基本但很有用的事件源类,它最低限度地揭示了足够的复杂性来说明这一点:

  1. public class EventSource
  2. {
  3. public event EventHandlerEvent = delegate { };
  4. public void Raise()
  5. {
  6. Event(this, EventArgs.Empty);
  7. }
  8. }

对好奇那个奇怪的空委托初始化方法(delegate { })的人来说,这是一个用来确保事件总被初始化的技巧,这样就可以不必每次在使用它之前都要检查它是否不为NULL。

触发垃圾收集的实用方法

在.net中,垃圾收集以一种不确定的方式触发。这对我们的实验很不利,我们的实验需要以一种确定的方式跟踪对象的状态。

所以,我们必须定期触发自己的垃圾收集操作,同时避免复制管道代码,管道代码已经在在一个特定的方法中释放:

  1. static void TriggerGC()
  2. {
  3. Console.WriteLine("Starting GC.");
  4. GC.Collect();
  5. GC.WaitForPendingFinalizers();
  6. GC.Collect();
  7. Console.WriteLine("GC finished.");
  8. }

虽然不是很复杂,但是如果你不是很熟悉这种模式,还是有必要小小解释一下:

  • 第一个 GC.Collect() 触发.net的CLR垃圾收集器,对于负责清理不再使用的对象,和那些类中没有终结器(即c#中的析构函数)的对象,CLR垃圾收集器足够胜任

  • GC.WaitForPendingFinalizers() 等待其他对象的终结器执行;我们需要这样做,因为,你将看到我们使用终结器方法去追踪我们的对象在什么时候被收集的

  • 第二个GC.Collect() 确保新生成的对象也被清理了

引入问题

首先让我们试着通过一些理论,最重要的是还有一个演示的帮助,去了解事件监听器有哪些问题。

背景

一个对象要想被作为事件侦听器,需要将其实例方法之一登记为另一个能够产生事件的对象(即事件源)的事件处理程序,事件源必须保持一个到事件侦听器对象的引用,以便在事件发生时调用此侦听器的处理方法。

这很合理,但如果这个引用是一个 强引用,则侦听器会作为事件源的一个依赖 从而不能作为垃圾回收,即使引用它的最后一个对象是事件源。

下面详细图解在这下面发生了什么:

事件处理问题

这将不是一个问题,如果你可以控制listener object的生命周期,你可以取消对事件源的订阅当当你不再需要listener,常常可以使用disposable pattern(用后就扔的模式)。

但是如果你不能在listener生命周期内验证单点响应,在确定性的方式中你不能把它处理掉,你必须依赖GC处理...这将从不会考虑你所准备的对象,只要事件源还存在着!

例子

理论都是好的,但还是让我们看看问题和真正的代码。

这是我们勇敢的时间监听器,还有点幼稚,我们很快知道为什么:

  1. public class NaiveEventListener
  2. {
  3. private void OnEvent(object source, EventArgs args)
  4. {
  5. Console.WriteLine("EventListener received event.");
  6. }
  7. public NaiveEventListener(EventSource source)
  8. {
  9. source.Event += OnEvent;
  10. }
  11. ~NaiveEventListener()
  12. {
  13. Console.WriteLine("NaiveEventListener finalized.");
  14. }
  15. }

用一个简单例子来看看怎么实现运作:

  1. Console.WriteLine("=== Naive listener (bad) ===");
  2. EventSource source = new EventSource();
  3. NaiveEventListener listener = new NaiveEventListener(source);
  4. source.Raise();
  5. Console.WriteLine("Setting listener to null.");
  6. listener = null;
  7. TriggerGC();
  8. source.Raise();
  9. Console.WriteLine("Setting source to null.");
  10. source = null;
  11. TriggerGC();

输出:

  1. EventListener received event.
  2. Setting listener to null.
  3. Starting GC.
  4. GC finished.
  5. EventListener received event.
  6. Setting source to null.
  7. Starting GC.
  8. NaiveEventListener finalized.
  9. 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 ,那么你将从简单的实现受益

  • 另外,你必须依靠一点人为的技巧手段

传统方式

(这两个位于WindowBase程序集,你将需要参考你自己的如果你不在开发WPF项目,你应该准确的参考WindowBase)

因此这有两步处理.

首先通过继承WeakEventManager来实现一个自定义事件管理器:

  • 重写 StartListening 和 StopListening 方法,分别注册一个新的handler和注销一个已存在的; 它们将被WeakEventManager基类使用。

  • 提供两个方法来访问listener列表, 命名为 “AddListener” 和 “RemoveListener“,给自定义事件管理器的使用者使用。

  • 通过在自定义事件管理器上暴露一个静态属性,提供一个方式去获得当前线程的事件管理器。

之后使listenr实现IWeakEventListenr接口:

  • 实现 ReceiveWeakEvent 方法

  • 尝试去处理这个事件

  • 如果无误的处理好事件,将返回true

有很多要说的,但是可以相对地转换成一些代码:

首先是自定义弱事件管理器:

  1. public class EventManager : WeakEventManager
  2. {
  3. private static EventManager CurrentManager
  4. {
  5. get
  6. {
  7. EventManager manager = (EventManager)GetCurrentManager(typeof(EventManager));
  8. if (manager == null)
  9. {
  10. manager = new EventManager();
  11. SetCurrentManager(typeof(EventManager), manager);
  12. }
  13. return manager;
  14. }
  15. }
  16. public static void AddListener(EventSource source, IWeakEventListener listener)
  17. {
  18. CurrentManager.ProtectedAddListener(source, listener);
  19. }
  20. public static void RemoveListener(EventSource source, IWeakEventListener listener)
  21. {
  22. CurrentManager.ProtectedRemoveListener(source, listener);
  23. }
  24. protected override void StartListening(object source)
  25. {
  26. ((EventSource)source).Event += DeliverEvent;
  27. }
  28. protected override void StopListening(object source)
  29. {
  30. ((EventSource)source).Event -= DeliverEvent;
  31. }
  32. }

之后是事件listener:

  1. public class LegacyWeakEventListener : IWeakEventListener
  2. {
  3. private void OnEvent(object source, EventArgs args)
  4. {
  5. Console.WriteLine("LegacyWeakEventListener received event.");
  6. }
  7. public LegacyWeakEventListener(EventSource source)
  8. {
  9. EventManager.AddListener(source, this);
  10. }
  11. public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e)
  12. {
  13. OnEvent(sender, e);
  14. return true;
  15. }
  16. ~LegacyWeakEventListener()
  17. {
  18. Console.WriteLine("LegacyWeakEventListener finalized.");
  19. }
  20. }

检查下:

  1. Console.WriteLine("=== Legacy weak listener (better) ===");
  2. EventSource source = new EventSource();
  3. LegacyWeakEventListener listener = new LegacyWeakEventListener(source);
  4. source.Raise();
  5. Console.WriteLine("Setting listener to null.");
  6. listener = null;
  7. TriggerGC();
  8. source.Raise();
  9. Console.WriteLine("Setting source to null.");
  10. source = null;
  11. TriggerGC();

输出:

  1. LegacyWeakEventListener received event.
  2. Setting listener to null.
  3. Starting GC.
  4. LegacyWeakEventListener finalized.
  5. GC finished.
  6. Setting source to null.
  7. Starting GC.
  8. 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> 自己处理泛型, 不用去一个个实现新事件管理器.

而且代码还简单和可读:

  1. public class WeakEventListener
  2. {
  3. private void OnEvent(object source, EventArgs args)
  4. {
  5. Console.WriteLine("WeakEventListener received event.");
  6. }
  7. public WeakEventListener(EventSource source)
  8. {
  9. WeakEventManager.AddHandler(source, "Event", OnEvent);
  10. }
  11. ~WeakEventListener()
  12. {
  13. Console.WriteLine("WeakEventListener finalized.");
  14. }
  15. }

简单的一行代码,真简洁.

其他实现的使用也是相似的, 就是装入所有东西到事件listener类里:

  1. Console.WriteLine("=== .Net 4.5 weak listener (best) ===");
  2. EventSource source = new EventSource();
  3. WeakEventListener listener = new WeakEventListener(source);
  4. source.Raise();
  5. Console.WriteLine("Setting listener to null.");
  6. listener = null;
  7. TriggerGC();
  8. source.Raise();
  9. Console.WriteLine("Setting source to null.");
  10. source = null;
  11. TriggerGC();

输出也是肯定正确的:

  1. WeakEventListener received event.
  2. Setting listener to null.
  3. Starting GC.
  4. WeakEventListener finalized.
  5. GC finished.
  6. Setting source to null.
  7. Starting GC.
  8. GC finished.

预期结果也跟之前一样,还有什么问题?!

结论

正如你看到的,在.Net上实现弱事件模式 是十分直接, 特别在 .Net 4.5.

如果你没有用.Net 4.5来实现,将需要一堆代码, 你可能不去用任何模式而是直接使用C# (+= and -=), 看看是否有内存问题,如果注意到泄露,还需要花必要的时间去实现一个。

但是用 .Net 4.5, 它是自由和简洁,而且由框架管理, 你可以毫无顾虑的选择它, 尽管没有 C# 语法 “+=” 和 “-=” 的酷, 但是语义是清晰的,这才是最重要的.

【转载】详细解读C#中的 .NET 弱事件模式的更多相关文章

  1. C#中的 .NET 弱事件模式

    引言 你可能知道,事件处理是内存泄漏的一个常见来源,它由不再使用的对象存留产生,你也许认为它们应该已经被回收了,但不是,并有充分的理由. 在这个短文中(期望如此),我会在 .Net 框架的上下文事件处 ...

  2. 在WPF中应用弱事件模式

    http://www.cnblogs.com/rickiedu/archive/2007/03/15/676021.html 在wpf中应用弱事件模式        感谢VS 的Intellisens ...

  3. WPF程序中的弱事件模式

    在C#中,得益于强大的GC机制,使得我们开发程序变得非常简单,很多时候我们只需要管使用,而并不需要关心什么时候释放资源.但是,GC有的时并不是按照我们所期望的方式工作. 例如,我想实现一个在窗口的标题 ...

  4. 【转载】Asp.Net中应用程序的事件响应次序

    Asp.Net应用程序事件响应次序是指Application事件的相应次序,涉及到的事件包括Application_Start事件.BeginRequest事件.AuthenticateRequest ...

  5. 详细解读Android中的搜索框—— SearchView

    以前总是自己写的 今天看看别人做的 本篇讲的是如何用searchView实现搜索框,其实原理和之前的没啥差别,也算是个复习吧. 一.Manifest.xml 这里我用一个activity进行信息的输入 ...

  6. [转载] 详细讲解Hadoop中的简单数据库HBase

    转载自http://www.csdn.net/article/2010-11-28/282614 数据模型 HBase数据库使用了和Bigtable非常相似的数据模型.用户在表格里存储许多数据行.每个 ...

  7. 详细解读Android中的搜索框(三)—— SearchView

    本篇讲的是如何用searchView实现搜索框,其实原理和之前的没啥差别,也算是个复习吧. 一.Manifest.xml 这里我用一个activity进行信息的输入和展示,配置方式还是老样子,写一个输 ...

  8. 详细解读Python中的__init__()方法

    init()方法意义重大的原因有两个.第一个原因是在对象生命周期中初始化是最重要的一步:每个对象必须正确初始化后才能正常工作.第二个原因是init()参数值可以有多种形式. 因为有很多种方式为init ...

  9. 详细解读css中的浮动以及清除浮动的方法

    对于前端初学者来说,css浮动部分的知识是一块比较难以理解的部分,下面我将把我学习过程中的心得分享给大家. 导读:   1.css块级元素讲解 2.css中浮动是如何产生的 3.出现浮动后,如何清除浮 ...

随机推荐

  1. ubuntu下使用ngrok外网映射

    好久之前想搞明白这个事情,可是就是不知道这个词叫外网映射,所以也一直不知怎么做,在慕课网看用java开发微信公众号的时候教程里提到了外网映射,查了一些资料终于把本地给映射到外网了,直接变成了80端口, ...

  2. Alfred(未完待续)

    之前曾经有写过一篇QuickSilver的博文,大力称赞它是Mac下的神器,但是现在,QS光荣下岗了,因为我找到了另外一款比QS更加好用,更加神器的APP:Alfred小帽子. 软件名:Alfred ...

  3. js冲突怎么解决

    a.最容易出现的就是js的命名冲突①.变量名冲突变量有全局变量和局部变量当全局变量变量和局部变量名称一致时,就会js冲突,由于变量传递数值或地址不同就会产生JavaScript错误,甚至死循环.②.方 ...

  4. 超级好用的国际汇兑平台--Transferwise

    一年的CSC留学快结束了,手里还剩了些积攒下来的美元.就国内那点博士的工资,出来一趟好不容易领了点美元可不想都给银行汇兑的手续费给吞了去. 这两天英国退欧,英镑大跌,美元有涨,是个把手里的美元寄回国换 ...

  5. BZOJ2253 2010 Beijing wc 纸箱堆叠 CDQ分治

    这题之前度娘上没有CDQ分治做法,gerwYY出来以后写了一个.不过要sort3遍,常数很大. gerw说可以类似划分树的思想优化复杂度,但是蒟蒻目前不会划分树(会了主席树就懒得去弄了). 嗯 将me ...

  6. Windows版本搭建安装React Native环境配置及相关问题

    此文档整理参考地址: http://www.lcode.org/%E5%8F%B2%E4%B8%8A%E6%9C%80%E8%AF%A6%E7%BB%86windows%E7%89%88%E6%9C% ...

  7. Java Web中资源的访问路径

    在web应用中,以“/”开头的是绝对路径,不以“/”开头的是相对路径.   在服务器端,通常都使用绝对路径.例如web.xml.struts.xml.servlet等的访问路径都是以“/”开始. 服务 ...

  8. [Windows] Adobe Photoshop CC 2015官方原版下载 附破解补丁&破解教程

      Photoshop自CS6以后改为CC,目前Photoshop CC 2015是最新版,发布日期为2015年6月. <ignore_js_op> 下载安装主程序: 主程序及补丁下载地址 ...

  9. uboot中的mmc命令

    一:mmc的命令例如以下: 1:对mmc读操作 mmc read addr blk# cnt 2:对mmc写操作 mmc write addr blk# cnt 3:对mmc擦除操作 mmc eras ...

  10. 初步掌握HDFS的架构及原理

    目录 HDFS 是做什么的 HDFS 从何而来 为什么选择 HDFS 存储数据 HDFS 如何存储数据 HDFS 如何读取文件 HDFS 如何写入文件 HDFS 副本存放策略 Hadoop2.x新特性 ...