WPF: 深入理解 Weak Event 模型
2018-02-24 13:46
134 查看
在之前写的一篇文章(XAML: 自定义控件中事件处理的最佳实践)中,我们曾提到了在 .NET 中如果事件没有反注册,将会引起内存泄露。这主要是因为当事件源会对事件监听者产生一个强引用,导致事件监听者无法被垃圾回收。
在这篇文章中,我们首先将进一步说明内存泄露的问题;然后,我们会重点介绍 .NET 中的 Weak Event 模型以及它的应用;之所以使用 Weak Event 模型就是为了解决常规事件中所引起的内存泄露;最后,我们会自己来实现 Weak Event 模型。
而事件源之所以对事件监听者产生强引用,这是由于事件是基于委托,当为某事件注册了监听时,该事件对应的委托会存储对事件监听者的引用。要解决这个问题,只能通过反注册事件。
在这个场景中,Model 作为数据源,而 UI 作为事件监听者。如果按照常规事件来处理 Model 中的 PropertyChanged 事件,那么,Model 就会对 UI 上的控件产生一个强引用。甚至在控件从可视化树 (VisualTree) 上移除后,只要 Model 的生命周期还没结束,那么控件就一定不能被回收。
可想而之,当 UI 中使用数据绑定的控件在 VisualTree 上经常变化时(添加或移除),造成的内存泄露问题将会非常严重。
因此,WPF 引入了 Weak Event 模式来解决这个问题。
WeakEventManager 是一个抽象类,包含两个抽象方法和一些受保护方法,因此要使用它,就需要创建它的派生类。
除了 WeakEventManager,还要用到 IWeakEventListener 接口,需要处理事件的类要实现这个接口,它包含一个方法:
ReceiveWeakEvent 方法可以得到 EventManager 的类型以及事件源和事件参数,它返回 bool 类型,用于指明传递过来的事件是否被处理。
正是借助于这些 WeakEventManager 来实现了 Weak Event 模型,解决了常规事件强引用的问题,从而使得当控件的生命周期早于 Model 的生命周期时,它们能够被垃圾回收。
事件源的生命周期比事件监听者的长;
事件源和事件监听者的生命周期不明确;
事件监听者不知道该何时移除事件监听或者不容易移除;
很明显,前面提到的关于数据绑定的问题是属于第一种情况。
实现 Weak Event 模型有三种方法:
使用 WeakEventManager<TEventSource,TEventArgs> ;
创建自定义 WeakEventManager 类;
使用现有的 WeakEventManager;
在开始实现之前,我们首要需要有一个事件源和事件。假定我们有一个 ValueObject 类,它有一个事件 ValueChanged,用来表示值已经更改;并且,我们再明确一下实现 Weak Event 模型的目的:去除 ValueObject 对监听 ValueChanged 事件对象的强引用,解决内存泄露。
以下是事件源的相关代码:
补充一点:为事件源实现 Weak Event 模型,事件源本身不需要作任何改动。
上述代码的运行结果如下:
在 AddHanlder 方法中,我们需要手工指明要监听的事件名,所以,我们可以看出,在 AddHanlder 方法内部会用到反射,因此会略微耗一些性能。而接下来将要提到的自定义 WeakEventManager 类,则不存在这个问题,不过,它写的代码要更多。
在上面的代码中,我们看到,由于自定义的 WeakEventManager 类作了事件的监听者,所以事件源不再引用事件监听者了,而是现在的 WeakEventManager。
然后,继续在它里面添加以下代码,用于方便处理事件监听:
说明:这里我们定义了一个静态只读属性,返回当前 WeakEventManager 的单例,并利用它来调用其基类的对应方法。
接下来,我们创建一个类 ValueChangedListener,并使它实现 IWeakEventListener 接口。这个类负责处理由 WeakEventManager 传递过来的事件:
在 ReceiveWeakEvent 方法中会调用 HandleValueChangedEvent 方法来处理传给 Listener 的事件。使用:
当执行到最后一句代码时,会输出如下结果:
举例来说,有一个 Person 类,我们需要关注它的属性值变化,那么就可以为它实现 INotifyPropertyChanged,如下:
注意:现在讨论的场景不仅用于 WPF ,也适用于其它任何平台,只要你有同样的需求:监测属性值变化。
然后,我们再创建一个类 PropertyChangedEventListener 用于响应 PropertyChanged 事件;像上面的 ValueChangedListener 类一样,这个类也要实现 IWeakEventListener 接口,代码如下:
在 ReceiveWeakEvent 方法中,我们可以添加当某属性更改时,如何来处理。其实,我们在这里已经简单地模拟了 WPF 中通过数据绑定更新 UI 的思路,不过真正的情况一定会比这要复杂。来看如何使用:
输出结果:
如果你在开发过程中,遇到了类似的场景或者同样的问题,也可以尝试使用 Weak Event 来解决。
参考资料:
Weak Event Patterns
WeakEventManager Class
Preventing Event-based Memory Leaks – WeakEventManager
源码下载
在这篇文章中,我们首先将进一步说明内存泄露的问题;然后,我们会重点介绍 .NET 中的 Weak Event 模型以及它的应用;之所以使用 Weak Event 模型就是为了解决常规事件中所引起的内存泄露;最后,我们会自己来实现 Weak Event 模型。
一、再谈内存泄露
1. 原因
我们通常会这样为事件添加事件监听: <source>.<event> += <listener-delegate> 。这样注册事件会使事件源对事件监听者产生一个强引用(如下图)。即使事件监听者不再使用时,它也无法被垃圾回收,从而引起了内存泄露。而事件源之所以对事件监听者产生强引用,这是由于事件是基于委托,当为某事件注册了监听时,该事件对应的委托会存储对事件监听者的引用。要解决这个问题,只能通过反注册事件。
2. 具体问题
一个具体的例子是,对于 XAML 应用中的数据绑定,我们会为 Model 实现 INotifyPropertyChanged 接口,这个接口里面包含一个事件:PropertyChanged。当这个事件被触发时,那么表示属性值发生了改变,这时 UI 上绑定此属性的控件的值也要跟着变化。在这个场景中,Model 作为数据源,而 UI 作为事件监听者。如果按照常规事件来处理 Model 中的 PropertyChanged 事件,那么,Model 就会对 UI 上的控件产生一个强引用。甚至在控件从可视化树 (VisualTree) 上移除后,只要 Model 的生命周期还没结束,那么控件就一定不能被回收。
可想而之,当 UI 中使用数据绑定的控件在 VisualTree 上经常变化时(添加或移除),造成的内存泄露问题将会非常严重。
因此,WPF 引入了 Weak Event 模式来解决这个问题。
二、Weak Event 模型
1. WeakEventManager 与 IWeakEventListener
Weak Event 模型主要解决的问题就是内存泄露。它通过 WeakEventManager 来实现;WeakEventManager 为作事件源和事件监听者的“中间人”,当事件源的事件触发时,由它负责向事件监听者传递事件。而 WeakEventManager 对事件监听者的引用是弱引用,因此,并不影响事件监听者被垃圾回收。如下图:WeakEventManager 是一个抽象类,包含两个抽象方法和一些受保护方法,因此要使用它,就需要创建它的派生类。
public abstract class WeakEventManager : DispatcherObject { protected static WeakEventManager GetCurrentManager(Type managerType); protected static void SetCurrentManager(Type managerType, WeakEventManager manager); protected void DeliverEvent(object sender, EventArgs args); protected void ProtectedAddHandler(object source, Delegate handler); protected void ProtectedAddListener(object source, IWeakEventListener listener); protected void ProtectedRemoveHandler(object source, Delegate handler); protected void ProtectedRemoveListener(object source, IWeakEventListener listener); protected abstract void StartListening(object source); protected abstract void StopListening(object source); }
除了 WeakEventManager,还要用到 IWeakEventListener 接口,需要处理事件的类要实现这个接口,它包含一个方法:
public interface IWeakEventListener { bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e); }
ReceiveWeakEvent 方法可以得到 EventManager 的类型以及事件源和事件参数,它返回 bool 类型,用于指明传递过来的事件是否被处理。
2. WPF 如何解决问题
在 WPF 中,对于 INotifyPropertyChanged 接口的 PropertyChanged 事件,以及 INotifyCollectionChanged 接口的 CollectionChanged 事件等,都有对应的 WeakEventManager 来处理它们。如下:正是借助于这些 WeakEventManager 来实现了 Weak Event 模型,解决了常规事件强引用的问题,从而使得当控件的生命周期早于 Model 的生命周期时,它们能够被垃圾回收。
三、实现 Weak Event 模型
实现我们自己的 Weak Event 模型非常简单,不过,首先,我们需要了解在什么情况下需要这么做,以下是几种使用场合:事件源的生命周期比事件监听者的长;
事件源和事件监听者的生命周期不明确;
事件监听者不知道该何时移除事件监听或者不容易移除;
很明显,前面提到的关于数据绑定的问题是属于第一种情况。
实现 Weak Event 模型有三种方法:
使用 WeakEventManager<TEventSource,TEventArgs> ;
创建自定义 WeakEventManager 类;
使用现有的 WeakEventManager;
在开始实现之前,我们首要需要有一个事件源和事件。假定我们有一个 ValueObject 类,它有一个事件 ValueChanged,用来表示值已经更改;并且,我们再明确一下实现 Weak Event 模型的目的:去除 ValueObject 对监听 ValueChanged 事件对象的强引用,解决内存泄露。
以下是事件源的相关代码:
#region 事件源 public delegate void ValueChangedHanlder(object sender, ValueChangedEventArgs e); public class ValueChangedEventArgs : EventArgs { public object NewValue { get; set; } } public class ValueObject { public event ValueChangedHanlder ValueChanged; public void ChangeValue(object newValue) { // 修改了值 ValueChanged?.Invoke(this, new ValueChangedEventArgs { NewValue = newValue }); } } #endregion 事件源
补充一点:为事件源实现 Weak Event 模型,事件源本身不需要作任何改动。
1. 使用 WeakEventManager<TEventSource,TEventArgs>
WeakEventManager<TEventSource, TEventArgs> 的两个泛型类型分别是事件源与事件参数,它有 AddHanlder/RemoveHanlder 两个方法。我们可以这样使用:private static void Main(string[] args) { var vo = new ValueObject(); WeakEventManager<ValueObject, ValueChangedEventArgs>.AddHandler(vo, "ValueChanged", OnValueChanged); // 触发事件 vo.ChangeValue("This is new value"); } private static void OnValueChanged(object sender, ValueChangedEventArgs e) { Console.WriteLine($"[Handler in Main] 值已改变,新值: {e.NewValue}"); }
上述代码的运行结果如下:
[Handler in Main] 值已改变,新值: This is new value
在 AddHanlder 方法中,我们需要手工指明要监听的事件名,所以,我们可以看出,在 AddHanlder 方法内部会用到反射,因此会略微耗一些性能。而接下来将要提到的自定义 WeakEventManager 类,则不存在这个问题,不过,它写的代码要更多。
2. 创建自定义 WeakEventManager 类
创建一个类,名为 ValueChangedEventManager,使它继承自 WeakEventManager,并重写其抽象方法:public class ValueChangedEventManager : WeakEventManager { protected override void StartListening(object source) { var vo = source as ValueObject; vo.ValueChanged += Vo_ValueChanged; } protected override void StopListening(object source) { var vo = source as ValueObject; vo.ValueChanged -= Vo_ValueChanged; } private void Vo_ValueChanged(object sender, ValueChangedEventArgs e) { // 向事件监听者传递事件 base.DeliverEvent(sender, e); } }
在上面的代码中,我们看到,由于自定义的 WeakEventManager 类作了事件的监听者,所以事件源不再引用事件监听者了,而是现在的 WeakEventManager。
然后,继续在它里面添加以下代码,用于方便处理事件监听:
/// <summary> /// 返回当前实例 /// </summary> public static ValueChangedEventManager CurrentManager { get { var mgr = GetCurrentManager(typeof(ValueChangedEventManager)) as ValueChangedEventManager; if (mgr == null) { mgr = new ValueChangedEventManager(); SetCurrentManager(typeof(ValueChangedEventManager), mgr); } return mgr; } } /// <summary> /// 添加事件监听 /// </summary> /// <param name="source"></param> /// <param name="eventListener"></param> public static void AddListener(object source, IWeakEventListener eventListener) { CurrentManager.ProtectedAddListener(source, eventListener); } /// <summary> /// 移除事件监听 /// </summary> /// <param name="source"></param> /// <param name="eventListener"></param> public static void RemoveListener(object source, IWeakEventListener eventListener) { CurrentManager.ProtectedRemoveListener(source, eventListener); }
说明:这里我们定义了一个静态只读属性,返回当前 WeakEventManager 的单例,并利用它来调用其基类的对应方法。
接下来,我们创建一个类 ValueChangedListener,并使它实现 IWeakEventListener 接口。这个类负责处理由 WeakEventManager 传递过来的事件:
public class ValueChangedListener : IWeakEventListener { public void HandleValueChangedEvent(object sender, ValueChangedEventArgs e) { Console.WriteLine($"[ValueChangedListener] 值已改变,新值: {e.NewValue}"); } /// <summary> /// 从 WeakEventManager 接收到事件,由 IWeakEventListener 定义 /// </summary> /// <param name="managerType"></param> /// <param name="sender"></param> /// <param name="e"></param> /// <returns></returns> public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) { // 对类型判断,如果是对应类型,则进行事件处理 if (managerType == typeof(ValueChangedEventManager)) { HandleValueChangedEvent(sender, (ValueChangedEventArgs)e); return true; } else { return false; } } }
在 ReceiveWeakEvent 方法中会调用 HandleValueChangedEvent 方法来处理传给 Listener 的事件。使用:
var vo = new ValueObject(); var eventListener = new ValueChangedListener(); ValueChangedEventManager.AddListener(vo, eventListener); // 触发事件 vo.ChangeValue("This is new value");
当执行到最后一句代码时,会输出如下结果:
[ValueChangedListener] 值已改变,新值: This is new value
3. 使用现有的 WeakEventManager
WPF 中包含了一些现成的 WeakEventManager,像上面图中的那些类,都派生于 WeakEventManager。如果你使用的是这些 EventManager 对应要处理的事件,则可以直接使用相应的 WeakEventManager。举例来说,有一个 Person 类,我们需要关注它的属性值变化,那么就可以为它实现 INotifyPropertyChanged,如下:
public class Person : INotifyPropertyChanged { private string _name; public event PropertyChangedEventHandler PropertyChanged; public string Name { get { return _name; } set { _name = value; RaisePropertyChanged(nameof(Name)); } } private void RaisePropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } }
注意:现在讨论的场景不仅用于 WPF ,也适用于其它任何平台,只要你有同样的需求:监测属性值变化。
然后,我们再创建一个类 PropertyChangedEventListener 用于响应 PropertyChanged 事件;像上面的 ValueChangedListener 类一样,这个类也要实现 IWeakEventListener 接口,代码如下:
/// <summary> /// 监听并处理 PropertyChanged 事件 /// </summary> public class PropertyChangedEventListener : IWeakEventListener { public bool ReceiveWeakEvent(Type managerType, object sender, EventArgs e) { if (managerType == typeof(PropertyChangedEventManager)) { // 对事件进行处理,如更新 UI 中对应绑定的值 Console.WriteLine($"[PropertyChangedEventListener] 此属性值已改变: { (e as PropertyChangedEventArgs).PropertyName}"); return true; } { return false; } } }
在 ReceiveWeakEvent 方法中,我们可以添加当某属性更改时,如何来处理。其实,我们在这里已经简单地模拟了 WPF 中通过数据绑定更新 UI 的思路,不过真正的情况一定会比这要复杂。来看如何使用:
var person = new Person(); var property = new PropertyChangedEventListener(); PropertyChangedEventManager.AddListener(person, property, nameof(person.Name)); // 通过修改属性值,触发 PropertyChanged 事件 person.Name = "Jim";
输出结果:
[PropertyChangedEventListener] 此属性值已改变: Name
总结
本文讨论了 WPF 中的 Weak Event 模型,它用于解决常规事件中内存泄露的问题。它的实现原理是使用 WeakEventManager 作为“中间人”而将事件源与事件监听者之间的强引用去除,当事件源中的事件触发后,由 WeakEventManager 将事件源和事件参数再传递监听者,而事件监听者在收到事件后,根据传过来的参数对事件作相应的处理。除此以外,我们也讨论了使用 Weak Event 模型的场景以及实现 Weak Event 模型的三种方法。如果你在开发过程中,遇到了类似的场景或者同样的问题,也可以尝试使用 Weak Event 来解决。
参考资料:
Weak Event Patterns
WeakEventManager Class
Preventing Event-based Memory Leaks – WeakEventManager
源码下载
相关文章推荐
- 深入理解javascript事件处理函数绑定三部曲(二)——传统处理函数绑定模型
- 深入理解盒子——模型文本垂直居中的N种方法 单行/多行文字(未知高度/固定高度)
- 利用Windbg深入理解变量的存储模型
- 深入理解C++对象模型之拷贝构造函数
- 深入理解JVM—JVM内存模型
- 深入理解JVM—JVM内存模型
- 深入理解JVM-内存模型
- 读 - 深入理解java虚拟机 - 笔记(二) - java内存模型与线程(12章)-处理器的内存模型
- 深入理解WPF布局子系统
- 深入理解 Laravel Eloquent(三)——模型间关系(关联)
- 深入理解JVM(二)——内存模型、可见性、指令重排序
- [java]深入理解JVM内存模型
- 【转】深入理解JVM—JVM内存模型
- 深入理解CSS系列(一):理解CSS的盒子模型
- 深入理解C++对象模型-对象的内存布局,vptr,vtable
- 深入理解Apache Mina(5)---- 配置Mina的 线程模型
- 深入理解JVM(一)——JVM内存模型
- 深入理解 Java final 变量的内存模型
- 【深入探索c++对象模型】抽象类和纯虚函数的理解
- 深入理解Apache Mina(5)---- 配置Mina的 线程模型