1. 程式人生 > >WPF: 深入理解 Weak Event 模型

WPF: 深入理解 Weak Event 模型

stand 生命周期 事件監聽 基類 改變 play 兩個 img turn

原文: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

源碼下載

WPF: 深入理解 Weak Event 模型