首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

.NET 陷阱:事件訂閱導致的記憶體洩漏與弱事件監聽器模式

     編輯:關於.NET

問題:事件訂閱的隱性強參考

寫下 source.StateChanged += observer.Handler 時,source 內部持有 observer 的強參考 (委派的 Target)。若 source 生命週期較長 (如單例、靜態事件、UI 視覺樹根節點),而 observer 應該被回收時,GC 無法回收 observer,造成:

  1. 記憶體洩漏:observer 及其引用的物件圖長駐記憶體。
  2. 邏輯錯誤:已失效的 observer 仍會收到事件,執行過時邏輯導致例外或資料不一致。

常見但不可靠的解法:手動取消訂閱

source.StateChanged -= observer.Handler 需開發者記住在正確時機 (如 Dispose、Unloaded) 調用。現實中極易遺漏、時機難把控、或因例外導致清理碼不執行。

優雅方案:弱事件監聽器

核心思想:**訂閱時不持有強參考,改持有 WeakReference;當目標被 GC 回收時,自動從事件源移除訂閱。**

WeakEventHandlerFactory 完整實作

提供兩種使用風格:

風格一:Lambda 捕獲移除邏輯 (C# 適用)

observed.SomeEvent += WeakEventHandlerFactory.MakeWeak(
    observer.SomeEventHandler,
    (handler, param) => observed.SomeEvent -= handler, // 移除委派
    null // 額外參數
);

風格二:反射式移除 (支援 C++/CLI 等無 Lambda 語言)

observed.SomeEvent += WeakEventHandlerFactory.MakeWeak2(
    observer.SomeEventHandler,
    observed,      // 事件源
    "SomeEvent"    // 事件名稱
);

核心類別結構

public static class WeakEventHandlerFactory {
    // 入口:建立弱委派
    public static EventHandler<E> MakeWeak<E>(EventHandler<E> handler, 
        UnregisterCallback<E> unregister, object param) where E : EventArgs { ... }

    // 內部弱處理器
    private class WeakEventHandler<T, E> : IWeakEventHandler<E> 
        where T : class where E : EventArgs {
        private readonly WeakReference _weakReference; // 弱參考目標
        private readonly OpenEventHandler _openHandler; // 開放委派 (無 Target)
        private UnregisterCallback<E> _unregister; // 移除回呼
        private readonly object _param;

        public WeakEventHandler(EventHandler<E> handler, ...) {
            _weakReference = new WeakReference(handler.Target);
            // 建立開放委派:static 方法簽名 (T @this, object sender, E e)
            _openHandler = (OpenEventHandler)Delegate.CreateDelegate(
                typeof(OpenEventHandler), null, handler.Method);
            Handler = Invoke; // 公開給事件源的強委派
        }

        private void Invoke(object sender, E e) {
            var target = (T)_weakReference.Target;
            if (target != null) {
                _openHandler.Invoke(target, sender, e); // 正常調用
            } else if (_unregister != null) {
                _unregister(Handler, _param); // 目標已死,自動移除訂閱
                _unregister = null; // 防重複
            }
        }
    }
}

關鍵技術點解析

  • WeakReference:不阻礙 GC,可檢測目標存活。
  • 開放委派:Delegate.CreateDelegate 建立不綁定 Target 的委派,調用時手動傳入 target,避免閉包捕獲強參考。
  • 反射建構泛型類別:typeof(WeakEventHandler<,>).MakeGenericType(...) 動態建構對應觀察者類型的泛型實例。
  • UnregisterCallback:委派簽名 (EventHandler<E> handler, object param),負責從事件源移除 handler。若傳 null,事件源仍殘留一個輕量委派 (指向已死的弱處理器),但不阻礙 GC,且後續觸發時弱處理器會自動清理自身。

最佳實踐建議

  1. 預設使用弱事件:尤其當觀察者生命週期短於事件源 (如 ViewModel 訂閱 Model、子視圖訂閱父容器事件)。
  2. 靜態事件/單例事件源:務必使用弱事件或實作 IDisposable 確保移除。
  3. 效能考量:弱事件有微小開銷 (WeakReference、開放委派調用、反射建構),高頻熱路徑 (如遊戲迴圈、即時渲染) 需評測。
  4. 替代方案:.NET 4.5+ WeakEventManager (WPF)、System.ComponentModel.INotifyPropertyChanged 擴充套件、Rx.NET WeakReference 操作符。

總結

事件訂閱是 .NET 記憶體洩漏的頭號隱形殺手。WeakEventHandlerFactory 以約 100 行程式碼封裝了「弱參考 + 自動移除」的完整模式,讓開發者以極低成本消除此類風險。掌握其原理 (WeakReference、開放委派、生命週期攔截),更能舉一反三,處理其他委派/回呼場景的循環參考問題。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved