WPF: 深入理解 Weak Event 模型

来源:https://www.cnblogs.com/wpinfo/archive/2018/02/24/understanding_weak_event.html
-Advertisement-
Play Games

在之前寫的一篇文章(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 模型有三種方法:

  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

 

源碼下載


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • auto關鍵字:1.C++98標準auto關鍵字的作用和C語言的相同,表示自動變數,是關於變數存儲位置的類型飾詞,通常不寫,因為局部變數的預設存儲就是auto 2.C++11標準中auto關鍵字不再表示變數的存儲類型,而是用於類型推導 (2.1)auto的基本用法 (2.2)auto和指針或者引用結 ...
  • 前言 很多項目, 都不是一個系統就做完了. 而是好多個系統, 相互協作來完成功能. 那, 系統與系統之間, 不可能完全獨立吧? 如: 在學校所用的管理系統中, 有學生系統, 資產系統, 宿舍系統等等. 當學期結束之後, 是否需要對已經結束的期次進行歸檔操作. 假如歸檔功能在學生系統中, 那點擊歸檔之 ...
  • final關鍵字的含義 在`Java final`,你將不能改變這個引用了,編譯器會檢查代碼,如果你試圖將變數再次初始化的話,編譯器會報編譯錯誤。 final變數 凡是對成員變數或者本地變數(在方法中的或者代碼塊中的變數稱為本地變數)聲明為 的都叫作 變數。 變數經常和 關鍵字一起使用,作為常量。 ...
  • 1. Scrapy框架 Scrapy是python下實現爬蟲功能的框架,能夠將數據解析、數據處理、數據存儲合為一體功能的爬蟲框架。 2. Scrapy安裝 1. 安裝依賴包 2. 安裝scrapy 註意事項:scrapy和twisted存在相容性問題,如果安裝twisted版本過高,運行scrapy ...
  • RuntimeException也可以給throws 非運行異常(編譯異常)throw 一定需要throws 異常,以待捕獲或繼續拋出,是因為運行時異常一旦發生,程式會停止 運行時異常 jvm會自動補throws,所以不寫也不會出錯,寫上也行 子父類異常問題 子類異常不能大於父類異常 父類無異常,子 ...
  • Spring提供了一套管理項目中的事務的機制 以前寫過一篇簡單的介紹事務的隨筆:http://www.cnblogs.com/xuyiqing/p/8430214.html 還有一篇Hibernate的事務管理:http://www.cnblogs.com/xuyiqing/p/8449167.ht ...
  • 一、高效定位代碼 1、跳轉 1、項目之間的跳轉 Next Project Window 快捷鍵 Ctrl + Alt + 左方括弧。 Previous Project Window 快捷鍵 Ctrl + Alt + 左方括弧。 2、文件之間的跳轉 1、Find Action 快捷鍵 Ctrl + S ...
  • .NET Core UI框架Avalonia,Avalonia是一個基於WPF XAML的跨平臺UI框架,並支持多種操作系統:Windows(.NET Framework,.NET Core),Linux(GTK),MacOS,Android和iOS。 Avalonia目前處於測試階段。 GitHu ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...