【转载】详细解读C#中的 .NET 弱事件模式
你可能知道,事件处理是内存泄漏的一个常见来源,它由不再使用的对象存留产生,你也许认为它们应该已经被回收了,但不是,并有充分的理由。 在这个短文中(期望如此),我会在 .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# 语法 “+=” 和 “-=” 的酷, 但是语义是清晰的,这才是最重要的.
【转载】详细解读C#中的 .NET 弱事件模式的更多相关文章
- C#中的 .NET 弱事件模式
引言 你可能知道,事件处理是内存泄漏的一个常见来源,它由不再使用的对象存留产生,你也许认为它们应该已经被回收了,但不是,并有充分的理由. 在这个短文中(期望如此),我会在 .Net 框架的上下文事件处 ...
- 在WPF中应用弱事件模式
http://www.cnblogs.com/rickiedu/archive/2007/03/15/676021.html 在wpf中应用弱事件模式 感谢VS 的Intellisens ...
- WPF程序中的弱事件模式
在C#中,得益于强大的GC机制,使得我们开发程序变得非常简单,很多时候我们只需要管使用,而并不需要关心什么时候释放资源.但是,GC有的时并不是按照我们所期望的方式工作. 例如,我想实现一个在窗口的标题 ...
- 【转载】Asp.Net中应用程序的事件响应次序
Asp.Net应用程序事件响应次序是指Application事件的相应次序,涉及到的事件包括Application_Start事件.BeginRequest事件.AuthenticateRequest ...
- 详细解读Android中的搜索框—— SearchView
以前总是自己写的 今天看看别人做的 本篇讲的是如何用searchView实现搜索框,其实原理和之前的没啥差别,也算是个复习吧. 一.Manifest.xml 这里我用一个activity进行信息的输入 ...
- [转载] 详细讲解Hadoop中的简单数据库HBase
转载自http://www.csdn.net/article/2010-11-28/282614 数据模型 HBase数据库使用了和Bigtable非常相似的数据模型.用户在表格里存储许多数据行.每个 ...
- 详细解读Android中的搜索框(三)—— SearchView
本篇讲的是如何用searchView实现搜索框,其实原理和之前的没啥差别,也算是个复习吧. 一.Manifest.xml 这里我用一个activity进行信息的输入和展示,配置方式还是老样子,写一个输 ...
- 详细解读Python中的__init__()方法
init()方法意义重大的原因有两个.第一个原因是在对象生命周期中初始化是最重要的一步:每个对象必须正确初始化后才能正常工作.第二个原因是init()参数值可以有多种形式. 因为有很多种方式为init ...
- 详细解读css中的浮动以及清除浮动的方法
对于前端初学者来说,css浮动部分的知识是一块比较难以理解的部分,下面我将把我学习过程中的心得分享给大家. 导读: 1.css块级元素讲解 2.css中浮动是如何产生的 3.出现浮动后,如何清除浮 ...
随机推荐
- 关于OC头文件互相引用的问题
在OC中头文件互相引用是很常见的一件事,如: A的头文件#import "B.h" 而B的头文件#import "A.h" 这个时候就会出现意想不到的问题.系统 ...
- Telephone directory - SGU 127(水)
题目大意:有一个电话簿,每页最多纪录K行电话,现在有N个电话要记录在电话薄上,要求同页的电话号码的首位要相同,电话簿的前两页是纪录的别的东西,问最少需要多少页电话簿. 分析:直接求首位数字有多少个即可 ...
- poj 3259 Wormholes【spfa判断负环】
Wormholes Time Limit: 2000MS Memory Limit: 65536K Total Submissions: 36729 Accepted: 13444 Descr ...
- c盘没有新建修改权限的,执行下面命令
cmd中执行: icacls c:\ /setintegritylevel M
- python_将一组数据展示成直方图(以list为例)
直接上代码: from matplotlib import pyplot as plt # 参数依次为list,抬头,X轴标签,Y轴标签,XY轴的范围 def draw_hist(myList,Tit ...
- C#将数据以XML格式写入Excel
本文转载:http://www.cnblogs.com/eflylab/archive/2008/09/21/1295580.html c#将数据导入Excel另类方法 今天公司突然给个Excel模版 ...
- C# 自动登录网页,浏览页面【转载】
需求:客户的数据同时存在在另外一个不可控的系统中,需要和当前系统同步. 思路:自动登录另外一个系统,然后抓取数据,同步到本系统中. 技术点:模拟用户登录:保存登录状态:抓取数据 /// <sum ...
- 实战DeviceIoControl 之中的一个:通过API訪问设备驱动程序
P.bhw98 { PADDING-RIGHT: 0px; PADDING-LEFT: 0px; FONT-SIZE: 9pt; PADDING-BOTTOM: 0px; MARGIN: 10px 0 ...
- SystemTap----常用变量、宏、函数和技巧
http://blog.csdn.net/moonvs2010/article/category/1570309
- linux的文件系统及节点表
linux的文件系统及节点表 一 linux的文件系统1 我们都知道当我们安装linux时会首先给系统分区,然后我们会把分区格式化成EXT3格式的文件系统.那么在linux系统中还有没有其他的文件系 ...