2010-05-11 65 views
11

問題:已註冊的事件處理程序將事件引用創建爲事件處理程序的實例。如果該實例無法註銷事件處理程序(大概是通過Dispose),那麼實例內存將不會被垃圾收集器釋放。使用WeakReference解決導致內存泄漏的.NET未註冊事件處理程序的問題

例子:

class Foo 
    { 
     public event Action AnEvent; 
     public void DoEvent() 
     { 
      if (AnEvent != null) 
       AnEvent(); 
     } 
    }   
    class Bar 
    { 
     public Bar(Foo l) 
     { 
      l.AnEvent += l_AnEvent; 
     } 

     void l_AnEvent() 
     { 

     }    
    } 

如果我實例化一個Foo,並通過這一個新的酒吧構造,然後讓Bar對象的旅途中,它不會被垃圾收集器釋放,因爲AnEvent登記。

我認爲這是內存泄漏,看起來就像我的舊C++日子。當然,我可以讓Bar IDisposable,在Dispose()方法中註銷事件,並確保在其實例上調用Dispose(),但爲什麼我必須這樣做?

我第一個問題,爲什麼事件與強引用?爲什麼不使用弱引用?事件用於抽象地通知對象另一個對象的變化。在我看來,如果事件處理程序的實例不再被使用(即沒有對事件的非事件引用),那麼它所註冊的任何事件都應該自動取消註冊。我錯過了什麼?

我看過WeakEventManager。哇,真是痛苦。使用起來不僅非常困難,而且其文檔也不夠充分(請參閱http://msdn.microsoft.com/en-us/library/system.windows.weakeventmanager.aspx - 注意到「繼承者註釋」部分,其中有6個模糊的項目符號)。

我在不同的地方看過其他討論,但沒有什麼我覺得我可以使用的。如下所述,我提出了一個基於WeakReference的更簡單的解決方案。我的問題是:這是否不符合要求,複雜程度要低得多?

class Foo 
    { 
     public WeakReferenceEvent AnEvent = new WeakReferenceEvent(); 

     internal void DoEvent() 
     { 
      AnEvent.Invoke(); 
     } 
    } 

    class Bar 
    { 
     public Bar(Foo l) 
     { 
      l.AnEvent += l_AnEvent; 
     } 

     void l_AnEvent() 
     { 

     } 
    } 

通知兩件事情::

要使用的解決方案,如下上述代碼被修改 1. Foo類被修改以兩種方式:事件被替換WeakReferenceEvent的一個實例,如下所示;並且事件的調用被改變。 2. Bar類是UNCHANGED。

無需子類WeakEventManager的,實施IWeakEventListener等

好了,到WeakReferenceEvent實施。這顯示在這裏。需要注意的是它採用了通用的WeakReference <牛逼>,我從這裏借:http://damieng.com/blog/2006/08/01/implementingweakreferencet

class WeakReferenceEvent 
{ 
    public static WeakReferenceEvent operator +(WeakReferenceEvent wre, Action handler) 
    { 
     wre._delegates.Add(new WeakReference<Action>(handler)); 
     return wre; 
    } 

    List<WeakReference<Action>> _delegates = new List<WeakReference<Action>>(); 

    internal void Invoke() 
    { 
     List<WeakReference<Action>> toRemove = null; 
     foreach (var del in _delegates) 
     { 
      if (del.IsAlive) 
       del.Target(); 
      else 
      { 
       if (toRemove == null) 
        toRemove = new List<WeakReference<Action>>(); 
       toRemove.Add(del); 
      } 
     } 
     if (toRemove != null) 
      foreach (var del in toRemove) 
       _delegates.Remove(del); 
    } 
} 

它的功能實在是微不足道。我重寫operator +來獲得+ =語法糖匹配事件。這會爲Action委託創建WeakReferences。這允許垃圾收集器釋放事件目標對象(在此示例中爲Bar),當沒有人持有該對象時。

在Invoke()方法中,只需運行弱引用並調用其目標操作即可。如果發現任何死亡(即垃圾收集)參考,請將其從列表中刪除。

當然,這隻適用於Action類型的代表。我試圖做出這個通用的,但跑到失蹤的地方T:委託在C#!

作爲替代方案,只需修改類WeakReferenceEvent是一個WeakReferenceEvent <Ť>,和替換動作<Ť>中的作用。修復編譯器錯誤,並且用戶可以像這樣被使用的類:

class Foo 
    { 
     public WeakReferenceEvent<int> AnEvent = new WeakReferenceEvent<int>(); 

     internal void DoEvent() 
     { 
      AnEvent.Invoke(5); 
     } 
    } 

與<牛逼>,和運營商的完整代碼 - (去除事件)如下所示:

class WeakReferenceEvent<T> 
{ 
    public static WeakReferenceEvent<T> operator +(WeakReferenceEvent<T> wre, Action<T> handler) 
    { 
     wre.Add(handler); 
     return wre; 
    } 
    private void Add(Action<T> handler) 
    { 
     foreach (var del in _delegates) 
      if (del.Target == handler) 
       return; 
     _delegates.Add(new WeakReference<Action<T>>(handler)); 
    } 

    public static WeakReferenceEvent<T> operator -(WeakReferenceEvent<T> wre, Action<T> handler) 
    { 
     wre.Remove(handler); 
     return wre; 
    } 
    private void Remove(Action<T> handler) 
    { 
     foreach (var del in _delegates) 
      if (del.Target == handler) 
      { 
       _delegates.Remove(del); 
       return; 
      } 
    } 

    List<WeakReference<Action<T>>> _delegates = new List<WeakReference<Action<T>>>(); 

    internal void Invoke(T arg) 
    { 
     List<WeakReference<Action<T>>> toRemove = null; 
     foreach (var del in _delegates) 
     { 
      if (del.IsAlive) 
       del.Target(arg); 
      else 
      { 
       if (toRemove == null) 
        toRemove = new List<WeakReference<Action<T>>>(); 
       toRemove.Add(del); 
      } 
     } 
     if (toRemove != null) 
      foreach (var del in toRemove) 
       _delegates.Remove(del); 
    } 
} 

希望這會幫助別人,當他們遇到神祕事件導致垃圾收集世界的內存泄漏!

+0

「我第一個問題是爲什麼事件用強引用來實現?」 - 在某些情況下,只有通過事件引用保持活動狀態的中間對象以及發佈者與訂戶之間的事件調用是非常有用的。例如,當您爲每個事件源需要單獨的實例時,這是一種有用的模式,並且您不知道在任何時間點將有多少事件源。 – 2010-05-11 23:09:22

+2

有用的文章,但沒有太多的問題!你有沒有考慮過這個博客呢? – 2010-05-11 23:15:51

+0

@Franci:好點。這是例外情況,與正常情況相反,你不覺得嗎?我已經使用了相當多的事件,並且尚未需要此功能。 – Eric 2010-05-11 23:19:30

回答

2

我找到了我的問題的答案,爲什麼這不起作用。是的,的確,我錯過了一個小細節:調用+ =來註冊事件(l.AnEvent + = l_AnEvent;)會創建一個隱式的Action對象。該對象通常只由事件本身(以及調用函數的堆棧)保存。因此,當調用返回並且垃圾收集器運行時,隱式創建的Action對象被釋放(現在只有弱引用指向它),並且事件未註冊。

A(疼痛)的解決方案是保持到操作對象的引用如下:

class Bar 
    { 
     public Bar(Foo l) 
     { 
      _holdAnEvent = l_AnEvent; 
      l.AnEvent += _holdAnEvent; 
     } 
     Action<int> _holdAnEvent; 
     ... 
    } 

這工作,但是移除溶液的簡單性。

2

當然這會對性能產生影響。

它有點像爲什麼在我的解決方案中引用其他程序集時,我可以使用反射動態讀取程序集並在其類型中進行相關調用?

因此,在短期... 您使用的2個原因很強的參考... 1.類型安全(不是真的在這裏適用) 2.性能

這又回到了仿製藥的類似辯論通過哈希表。 上次我看到這個論點被放到桌面上,但是這張海報正在尋找一個預先生成的msil,或許這可以爲您解決這個問題?

雖然還有一個想法...... 如果您附加了一個事件處理程序來說com對象事件會怎樣? 從技術上講,對象不被管理,所以如何知道什麼時候需要清理,這當然歸結爲框架如何處理範圍?

這篇文章自帶的「它在我的頭上garantee」,不承擔責任爲這個職位是如何描繪:)

0

有兩種模式,我知道的製作弱事件訂閱:一個是事件訂閱者對指向他的代表有強烈的參照,而發佈者對該代表的引用很弱。這具有需要通過弱參考來完成所有事件觸發的缺點;它可能會使發佈者不知道任何事件是否已過期。

另一種方法是讓每個對某個對象真正感興趣的人引用一個包裝器,而該包裝器又引用了「膽量」;事件處理程序只有對「膽量」的引用,並且膽量不包含對包裝的強烈參考。包裝器還保存對其Finalize方法將取消訂閱事件的對象的引用(最簡單的方法是使用一個簡單的類,其Finalize方法調用操作<布爾型>值爲False,並且其Dispose方法調用委託值爲True並禁止最終確定)。

這種方法的缺點是需要通過包裝器(一個額外的強引用)來完成主類上的所有非事件操作,但避免了除事件訂閱和取消訂閱之外的任何其他WeakReference。不幸的是,在標準事件中使用這種方法需要:(1)任何發佈一個訂閱事件的類必須具有線程安全的(最好是無鎖的)'remove'處理器,可以從Finalize線程安全地調用(2)用於事件取消訂閱的對象直接或間接引用的所有對象將保持半活動狀態,直到終結器運行後GC通過。使用不同的事件範例(例如通過調用返回IDisposable的函數來訂閱事件,可以使用它來取消訂閱)可以避免這些限制。

0

當你有一個事件處理程序,你有兩個對象:

  1. 類的對象。 (foo的一個實例)
  2. 表示您的事件處理程序的對象。 (例如,EventHandler的實例或Action的一個實例。)

您認爲自己有內存泄漏的原因是EventHandler(或Action)對象內部持有對您的Foo對象的引用。這將阻止您的Foo對象被收集。

現在,你爲什麼不寫一個WeakEventHandler?答案是可以,但你基本上確保:

  1. 你的委託(事件處理程序或行動的實例)從未有一個硬引用您的Foo對象
  2. 你WeakEventHandler持有強引用您的代理以及對您的Foo對象的弱引用
  3. 您有條款最終在弱引用變爲無效時取消註冊WeakEventHandler。這是因爲沒有辦法知道何時收集對象。

實際上,這是不值得的。這是因爲你必須權衡:

  • 你的事件處理方法將需要靜態的,並採取了對象作爲參數,這種方式不持有強引用您要收集的對象。
  • 您的WeakEventHandler和Action對象處於Gen 1或Gen 2的高風險狀態。這會在垃圾回收器中造成高負載。
  • WeakReferences包含一個GC句柄。這可能會對性能產生負面影響。

因此,確保正確取消註冊您的事件處理程序是一個更好的解決方案。語法更簡單,內存使用更好,應用程序性能更好。