WPF: 深入理解 Weak Event 模型

在之前写的一篇文章(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

源码下载

原文地址:https://www.cnblogs.com/wpinfo/p/understanding_weak_event.html

时间: 2024-08-24 07:42:14

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

Weak Event Patterns

https://msdn.microsoft.com/en-US/library/aa970850(v=vs.100).aspx In applications, it is possible that handlers that are attached to event sources will not be destroyed in coordination with the listener object that attached the handler to the source.

The .NET weak event pattern in C#

Introduction As you may know event handlers are a common source of memory leaks caused by the persistence of objects that are not used anymore, and you may think should have been collected, but are not, and for good reason. … Continue reading → Intro

Weak Event Manager

问题 通过传统的方式监听事件(即C#的+=语法),有可能会导致内存泄漏,原因是事件源会持有对事件Handler所在对象的强引用从而阻碍GC回收它,这样事件handler对象的生命周期受到了事件源对象的影响. 解决方案 此问题有两个解决办法:1) 确保通过-=语法反注册事件处理器 2)使用弱事件模式(Weak Event Pattern).本文主要讲解Weak Event Pattern. 在使用Weak Event Pattern时,主要涉及到两个类:WeakEventManager和IWeak

理解CSS盒子模型

什么是CSS的盒子模式呢?为什么叫它是盒子?先说说我们在网页设计中常听的属性名:内容(content).填充(padding).边框(border).边界(margin), CSS盒子模式都具备这些属性. 这些属性我们可以把它转移到我们日常生活中的盒子(箱子)上来理解,日常生活中所见的盒子也具有这些属性,所以叫它盒子模式.那么内容就是盒子里装的东西:而填充就是怕盒子里装的东西(贵重的)损坏而添加的泡沫或者其它抗震的辅料:边框就是盒子本身了:至于边界则说明盒子摆放的时候的不能全部堆在一起,要留一定

深入理解CSS盒子模型

前言:前阵子在做一个项目时,在页面布局方面遇到了一点小问题,于是上stackoverflow上求助.ifaou在帮助我解决我问题的同时,还推荐我阅读一篇有关CSS盒子模型的文章<The CSS Box Model>,阅读之后受益匪浅,才知道自己对盒子模型知识还是如此欠缺.恰逢学期末,项目验收后暂时告一段落,有空闲的时间.于是想把这篇文章翻译出来,一方面再给自己一点挑战和锻炼,另一方面也给大家参考,让更多的人受益. 这篇文章适合初级web设计朋友,让你对盒子模型有更近一步的理解.但是在阅读这篇文

深入理解Java内存模型之系列篇[转]

原文链接:http://blog.csdn.net/ccit0519/article/details/11241403 深入理解Java内存模型(一)——基础 并发编程模型的分类 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体).通信是指线程之间以何种机制来交换信息.在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递. see:命令式编程.函数式编程 在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存

编写高质量代码改善C#程序的157个建议——建议87:区分WPF和WinForm的线程模型

建议87:区分WPF和WinForm的线程模型 WPF和WinForm窗体应用程序都有一个要求,那就是UI元素(如Button.TextBox等)必须由创建它的那个线程进行更新.WinForm在这方面的限制并不是很严格,所以像下面这样的代码,在WinForm中大部分情况下还能运行(本建议后面会详细解释为什么会出现这种现象): private void buttonStartAsync_Click(object sender, EventArgs e) { Task t = new Task(()

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

参考Infoq上的这篇文章:Link <深入理解Java内存模型(一)--基础>

深入理解java内存模型

深入理解Java内存模型(一)——基础 并发编程模型的分类 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体).通信是指线程之间以何种机制来交换信息.在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递. 在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间通过写-读内存中的公共状态来隐式进行通信.在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过明确的发送消息来显式进行通信. 同步是指程序用于控制不同线程之