在之前寫的一篇文章(XAML: 自定義控制項中事件處理的最佳實踐)中,我們曾提到了在 .NET 中如果事件沒有反註冊,將會引起記憶體泄露。這主要是因為當事件源會對事件監聽者產生一個強引用,導致事件監聽者無法被垃圾回收。 在這篇文章中,我們首先將進一步說明記憶體泄露的問題;然後,我們會重點介紹 .NET 中 ...
在之前寫的一篇文章(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 模型有三種方法:
- 使用 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 來解決。
參考資料:
Preventing Event-based Memory Leaks – WeakEventManager