在之前写的一篇文章(XAML: 自定义控件中事件处理的最佳实践)中,我们曾提到了在 .NET 中如果事件没有反注册,将会引起内存泄露。这主要是因为当事件源会对事件监听者产生一个强引用,导致事件监听者无法被垃圾回收。

在这篇文章中,我们首先将进一步说明内存泄露的问题;然后,我们会重点介绍 .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 模型有三种方法:

  1. 使用 WeakEventManager<TEventSource,TEventArgs> ;
  2. 创建自定义 WeakEventManager 类;
  3. 使用现有的 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

源码下载

WPF: 深入理解 Weak Event 模型的更多相关文章

  1. Weak Event Patterns

    https://msdn.microsoft.com/en-US/library/aa970850(v=vs.100).aspx In applications, it is possible tha ...

  2. WPF:理解ContentControl——动态添加控件和查找控件

    WPF:理解ContentControl--动态添加控件和查找控件 我认为WPF的核心改变之一就是控件模型发生了重要的变化,大的方面说,现在窗口中的控件(大部分)都没有独立的Hwnd了.而且控件可以通 ...

  3. The .NET weak event pattern in C#

    Introduction As you may know event handlers are a common source of memory leaks caused by the persis ...

  4. 《深入理解Java内存模型》读书总结

    概要 文章是<深入理解Java内容模型>读书笔记,该书总共包括了3部分的知识. 第1部分,基本概念 包括"并发.同步.主内存.本地内存.重排序.内存屏障.happens befo ...

  5. 深入理解java内存模型系列文章

    转载关于java内存模型的系列文章,写的非常好. 深入理解java内存模型(一)--基础 深入理解java内存模型(二)--重排序 深入理解java内存模型(三)--顺序一致性 深入理解java内存模 ...

  6. 【Todo】【转载】深入理解Java内存模型

    提纲挈领地说一下Java内存模型: 什么是Java内存模型 Java内存模型定义了一种多线程访问Java内存的规范.Java内存模型要完整讲不是这里几句话能说清楚的,我简单总结一下Java内存模型的几 ...

  7. 深入理解Java内存模型(一)——基础(转)

    转自程晓明的"深入理解Java内存模型"的博客 http://www.infoq.com/cn/articles/java-memory-model-1 并发编程模型的分类 在并发 ...

  8. 理解CSS盒子模型

    概述 网页设计中常听的属性名:内容(content).填充(padding).边框(border).边界(margin),CSS盒子模型都具备这些属性,也主要是这些属性. 这些属性我们可以把它转移到我 ...

  9. <转>HTML+CSS总结/深入理解CSS盒子模型

    原文地址:http://www.chinaz.com/design/2010/1229/151993.shtml 前言:前阵子在做一个项目时,在页面布局方面遇到了一点小问题,于是上stackoverf ...

随机推荐

  1. iOS学习——键盘弹出遮挡输入框问题解决方案

    在iOS或Android等移动端开发过程中,经常遇到很多需要我们输入信息的情况,例如登录时要输入账号密码.查询时要输入查询信息.注册或申请时需要填写一些信息等都是通过我们键盘来进行输入的,在iOS开发 ...

  2. sizeof(extern类型数组)

    error:  #70: incomplete type is not allowed 用sizeof计算数组大小,编译器提示不允许使用不完整的类型.在keil上编译直接报错,拿到vs2010上编译可 ...

  3. YourPHP笔记

    http://blog.sina.com.cn/s/blog_7c54793101016qq1.htm 基础认识: Ø  yourphp安装为子目录时不可以以"yourphp"为文 ...

  4. 为什么要进行URL编码

    我们都知道Http协议中参数的传输是"key=value"这种简直对形式的,如果要传多个参数就需要用“&”符号对键值对进行分割.如"?name1=value1&a ...

  5. asp.net -mvc框架复习(10)-基于三层架构与MVC搭建项目框架

    一.三种模式比较 1.MVC框架(适合大型项目) (1).V视图 (网页部分) (2).M模型 (业务逻辑+数据访问+实体类) (3).C控制器 (介于M和V之间,起到引导作用) 2.三层架构 (1) ...

  6. 详解python中的__init__与__new__方法

    一.__init__和__new__方法执行的顺序? 在面向对象中介绍了关于对象创建的过程,我们知道__new__方法先于__init__方法执行. 二.__new__方法是什么? 首先,我们先来看下 ...

  7. Laravel添加代码自动提示功能

    在使用Laravel框架的时候,可能会碰上代码无法自动提示的情况,那么如何添加自动提示功能呢? 1,首先在composer.json中加入以下内容: "require": { &q ...

  8. linux_网站计量单位

    IP 独立IP数,是不同IP地址的计算机访问网站时被计算的总次数,独立IP数是衡量网站流量的一个重要指标,一般一天内相同IP地址的客户端访问网页只被计算为一次,记录独立IP的时间为一天或一个月,目前通 ...

  9. JDBC (一)

    1 JDBC 简介 sun公司为了简化.统一对数据库的操作,定义了一套java操作数据库的规范,称之为JDBC. 数据库厂商的驱动就是对JDBC的实现. 没有JDBC之前  vs 有JDBC之后 JD ...

  10. PHP中变量的销毁

    PHP的变量或对象的销毁可以分成显式销毁和隐式销毁: 1.显式销毁,当对象没有被引用时就会被销毁,所以我们可以unset或为其赋值NULL; 2.隐式销毁,PHP是脚本语言,在代码执行完最后一行时,所 ...