簡體   English   中英

使用WeakReference解決.NET未注冊事件處理程序導致內存泄漏的問題

[英]Using WeakReference to resolve issue with .NET unregistered event handlers causing memory leaks

問題:已注冊的事件處理程序創建從事件到事件處理程序實例的引用。 如果該實例未能注銷事件處理程序(通過Dispose,可能),那么垃圾收集器將不會釋放實例內存。

例:

    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構造函數,那么放開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 <T>: 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()方法中,只需運行弱引用並調用其Target Action。 如果找到任何死亡(即垃圾收集)引用,請從列表中刪除它們。

當然,這僅適用於Action類型的委托。 我嘗試制作這個通用的,但遇到了丟失的地方T:委托在C#!

作為替代方案,只需將類WeakReferenceEvent修改為WeakReferenceEvent <T>,並將Action替換為Action <T>。 修復編譯器錯誤,你有一個可以像這樣使用的類:

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

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

帶有<T>的完整代碼和運算符 - (用於刪除事件)如下所示:

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);
    }
}

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

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

一個(痛苦的)解決方案是保存對Action對象的引用,如下所示:

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

這有效,但刪除了解決方案的簡單性。

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

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

所以簡而言之......你使用強有力的參考有兩個原因...... 1.類型安全(這里不太適用)2。性能

這可以追溯到關於哈希表的泛型的類似爭論。 上次我看到那個論點擺到桌面上但是海報看起來是生成的msil預先錄制的,也許這可以讓你對這個問題有所啟發?

另一個想法...如果你附加一個事件處理程序來說一個com對象事件怎么辦? 從技術上講,該對象不受管理,所以它如何知道何時需要清理,當然這歸結為框架如何處理范圍?

這篇文章附帶“它在我頭腦中保證工作”,對這篇文章的描述沒有責任:)

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

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

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

現在,為什么不能寫一個WeakEventHandler? 答案是你可以,但你基本上必須確保:

  1. 您的委托(EventHandler或Action的實例)永遠不會對您的Foo對象進行硬引用
  2. 您的WeakEventHandler擁有對您的委托的強引用,以及對您的Foo對象的弱引用
  3. 當弱引用變為無效時,您有規定最終取消注冊WeakEventHandler。 這是因為無法知道何時收集對象。

在實踐中,這不值得。 這是因為你有權衡:

  • 您的事件處理程序方法需要是靜態的並將對象作為參數,這樣它就不會對您想要收集的對象保持強引用。
  • 您的WeakEventHandler和Action對象很有可能進入Gen 1或Gen 2.這將導致垃圾收集器的高負載。
  • WeakReferences持有GC句柄。 這可能會對性能產生負面影響。

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

我知道有兩種模式用於制作弱事件訂閱:一種是讓事件訂閱者對指向他的委托持有強引用,而發布者持有對該委托的弱引用。 這樣做的缺點是需要通過弱引用來完成所有事件發射; 它可能會向出版商發出任何事件是否已過期的通知。

另一種方法是給對象實際感興趣的每個人一個對包裝器的引用,而包裝器又包含對“guts”的引用; 事件處理程序只引用了“guts”,而guts沒有對包裝器的強引用。 包裝器還包含對一個對象的引用,該對象的Finalize方法將取消訂閱該事件(最簡單的方法是使用一個簡單的類,其Finalize方法調用一個值為False的Action <Boolean>,並且其Dispose方法調用該方法委托值為True並禁止最終化)。

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM