c# - 為什麼以及如何避免事件處理程序內存洩漏?





design-patterns memory-leaks event-handling (4)


事件實際上是事件處理程序的鏈接列表

當你對事件執行+ = new EventHandler時,如果這個特定的函數之前已經添加為偵聽器,那麼它無關緊要,它將在每個+ =時添加一次。

當事件發生時,它會逐個瀏覽鍊錶,並調用添加到此列表中的所有方法(事件處理程序),這就是為什麼即使頁面不再運行時仍然調用事件處理程序的原因他們活著(紮根),只要他們聯繫起來,他們就會活著。 所以他們會被調用,直到eventhandler被一個 - = new EventHandler解除。

看這裡

MSDN在這裡

我剛剛通過閱讀上的一些問題和答案意識到,在C#中使用+=添加事件處理程序(或者我猜,其他.net語言)會導致常見的內存洩漏......

過去我多次使用過這樣的事件處理程序,並且從未意識到它們可能導致或導致應用程序中的內存洩漏。

這是如何工作的(意思是,為什麼這會導致內存洩漏)?
我該如何解決這個問題? 正在使用-=相同的事件處理程序嗎?
有處理這種情況的常見設計模式或最佳實踐嗎?
示例:我應該如何處理具有多個不同線程的應用程序,使用許多不同的事件處理程序在UI上引發多個事件?

有沒有什麼好的和簡單的方法可以在已經構建的大型應用程序中有效地監控這些?




原因很簡單:當訂閱事件處理程序時,事件的發布 通過事件處理程序委託(假設委託是實例方法)持有對訂閱者的引用。

如果發布者的壽命比訂閱者長,那麼即使沒有其他用戶引用,它也會保持訂閱者活著。

如果您用相同的處理程序取消訂閱該事件,那麼是的,這將刪除處理程序和可能的洩漏。 然而,根據我的經驗,這實際上很少是一個問題 - 因為通常我發現發布者和訂閱者無論如何擁有大致相等的生命週期。

一個可能的原因......但根據我的經驗,這是相當誇張的。 你的里程可能會有所不同,當然......你只需要小心。




是的, -=足夠了,但是,跟踪每個分配的事件可能相當困難。 (詳情請看Jon的帖子)。 關於設計模式,看看弱勢事件模式




塞巴斯蒂安

你所問的是一個相當棘手的問題。 雖然你可能認為這只是一個問題,但你實際上一次提出幾個問題。 我會盡我所能知道我不得不掩飾它,並希望其他一些人也會加入來掩飾我可能會錯過的事情。

內部類:簡介

由於我不確定您在Java中使用OOP有多舒服,這將會遇到一些基礎知識。 內部類是當一個類定義被包含在另一個類中時。 基本上有兩種類型:靜態和非靜態。 這些之間的真正區別是:

  • 靜態內部類:
    • 被認為是“頂級”。
    • 不要求構造包含類的實例。
    • 如果沒有明確的引用,可能不會引用包含的類成員。
    • 有自己的一生。
  • 非靜態內部類:
    • 始終要求構造包含類的實例。
    • 自動具有對包含實例的隱式引用。
    • 可以在沒有參考的情況下訪問容器的類成員。
    • 終身不應該比集裝箱的壽命長。

垃圾收集和非靜態內部類

垃圾收集是自動的,但會嘗試根據是否認為它們正在使用來移除對象。 垃圾收集器非常聰明,但並不完美。 它只能確定是否正在使用某個對像是否有活動引用。

這裡真正的問題是,非靜態內部類的存活時間比它的容器更長。 這是因為對含有類的隱式引用。 這種情況發生的唯一方法是,如果包含類之外的對象保留對內部對象的引用,而不考慮包含對象。

這可能會導致內部對象處於活動狀態(通過引用),但對包含對象的引用已從所有其他對像中刪除。 因此,內部對象保持包含對象的活性,因為它始終會引用它。 這個問題是,除非它被編程,否則無法返回到包含對象來檢查它是否還活著。

這種認識的最重要的方面是它在活動中還是可繪製的都沒有區別。 使用非靜態內部類時,您必須始終有條不紊,並確保它們永遠不會超過容器的對象。 幸運的是,如果它不是你的代碼的核心對象,那麼洩漏可能比較小。 不幸的是,這些是最難找到的一些漏洞,因為它們很可能會被忽視,直到其中許多漏洞被洩露。

解決方案:非靜態內部類

  • 從包含對象獲取臨時引用。
  • 允許包含對象成為唯一一個保留對內部對象的長效引用。
  • 使用已建立的模式,如工廠。
  • 如果內部類不需要訪問包含類成員,請考慮將其轉換為靜態類。
  • 無論是否在活動中,請謹慎使用。

活動和觀點:介紹

活動包含大量可以運行和顯示的信息。 活動由特徵定義,他們必須具有View。 他們也有一些自動處理程序。 無論您是否指定它,“活動”都會隱式引用它所包含的“視圖”。

為了創建視圖,它必須知道在哪裡創建視圖,以及它是否有任何子視圖,以便它可以顯示。 這意味著每個View都有一個對Activity的引用(通過getContext() )。 而且,每個視圖都會保留其子對象的引用(即getChildAt() )。 最後,每個視圖保持對呈現的代表其顯示的呈現的位圖的引用。

每當你有一個活動(或活動上下文)的引用,這意味著你可以沿著佈局層次結構中的整個鏈。 這就是為什麼關於活動或視圖的內存洩漏是如此巨大的交易。 它可能是一次性洩漏的大量內存。

活動,視圖和非靜態內部類

鑑於以上有關內部類的信息,這些是最常見的內存洩漏,但也是最常見的內存洩漏。 儘管希望內部類可以直接訪問Activities類成員,但許多人願意將它們靜態化以避免潛在的問題。 活動和觀點的問題比這更深。

洩漏的活動,視圖和活動上下文

這一切都歸結於語境和生命週期。 有某些事件(如方向)會殺死活動上下文。 由於許多類和方法需要Context,因此開發人員有時會嘗試通過獲取對Context的引用並保存它來保存一些代碼。 恰巧我們必須創建許多用於運行我們的活動的對象,這些對象必須存在於活動生命週期之外,以便活動能夠完成它需要做的事情。 如果你的任何對像在被銷毀時碰巧有一個Activity的引用,它的Context或它的任何Views,那麼你剛剛洩露了該Activity和它的整個View樹。

解決方案:活動​​和觀點

  • 不惜一切代價避免對視圖或活動進行靜態引用。
  • 對活動上下文的所有引用應該是短暫的(函數的持續時間)
  • 如果您需要長壽命的Context,請使用Application Context( getBaseContext()getApplicationContext() )。 這些不會隱式地保留引用。
  • 或者,您可以通過覆蓋配置更改來限制活動的銷毀。 但是,這並不能阻止其他潛在的事件破壞活動。 雖然你可以做到這一點,但你仍然可以參考上述做法。

Runnables:簡介

Runnables其實並沒有那麼糟糕。 我的意思是,他們可能是,但實際上我們已經擊中了大部分危險區域。 Runnable是一個異步操作,它執行與創建線程無關的任務。 大多數可運行子程序都是從UI線程實例化的。 實質上,使用Runnable可以創建另一個線程,只需稍微更多的管理。 如果您將Runnable分類為標準類並遵循上述指導原則,則應該遇到一些問題。 現實是許多開發人員不這樣做。

出於易讀性,可讀性和邏輯程序流程的考慮,許多開發人員利用匿名內部類來定義它們的Runnables,例如上面創建的示例。 這會產生一個像上面輸入的例子。 一個匿名內部類基本上是一個離散的非靜態內部類。 您不必創建全新的定義,只需重寫適當的方法即可。 在所有其他方面,它是一個非靜態內部類,這意味著它保持對其容器的隱式引用。

可運行和活動/視圖

好極了! 這部分可以簡短! 由於Runnables在當前線程之外運行的事實,這些危險會導致長時間運行異步操作。 如果runnable在Activity或View中被定義為匿名內部類或非靜態內部類,那麼存在一些非常嚴重的危險。 這是因為,如前所述,它必須知道它的容器是誰。 輸入方向更改(或系統終止)。 現在回頭看看前面的部分,了解剛發生的事情。 是的,你的例子很危險。

解決方案:Runnables

  • 如果不破壞代碼的邏輯,請嘗試並擴展Runnable。
  • 如果它們必須是內部類,盡最大努力使擴展的Runnables成為靜態。
  • 如果您必須使用匿名運行列表,請避免在任何具有對正在使用的活動或視圖進行長時間引用的對像中創建它們。
  • 許多Runnables可以像AsyncTasks一樣容易。 考慮使用AsyncTask,因為這些默認情況下是VM管理的。

回答最終問題現在回答本文其他部分未直接解決的問題。 你問:“內部階級的對象什麼時候能比外部階級生存得更久?” 在我們做到這一點之前,讓我再強調一下:雖然您在活動中擔心這一點是正確的,但它可能導致任何地方洩漏。 我將提供一個簡單的示例(不使用活動)來示範。

以下是基本工廠的常見示例(缺少代碼)。

public class LeakFactory
{//Just so that we have some data to leak
    int myID = 0;
// Necessary because our Leak class is non-static
    public Leak createLeak()
    {
        return new Leak();
    }

// Mass Manufactured Leak class
    public class Leak
    {//Again for a little data.
       int size = 1;
    }
}

這是一個不常見的例子,但很簡單,可以證明。 這裡的關鍵是構造函數...

public class SwissCheese
{//Can't have swiss cheese without some holes
    public Leak[] myHoles;

    public SwissCheese()
    {//Gotta have a Factory to make my holes
        LeakFactory _holeDriller = new LeakFactory()
    // Now, let's get the holes and store them.
        myHoles = new Leak[1000];

        for (int i = 0; i++; i<1000)
        {//Store them in the class member
            myHoles[i] = _holeDriller.createLeak();
        }

    // Yay! We're done! 

    // Buh-bye LeakFactory. I don't need you anymore...
    }
}

現在,我們有洩漏,但沒有工廠。 儘管我們發布了工廠,但它仍將保留在內存中,因為每一個洩漏都會引用它。 外面的班級沒有數據也沒有關係。 這發生得比人們想像的要多得多。 我們不需要創作者,只需創作它。 所以我們暫時創建一個,但無限期地使用創作。

想像一下,當我們稍微改變構造函數時會發生什麼。

public class SwissCheese
{//Can't have swiss cheese without some holes
    public Leak[] myHoles;

    public SwissCheese()
    {//Now, let's get the holes and store them.
        myHoles = new Leak[1000];

        for (int i = 0; i++; i<1000)
        {//WOW! I don't even have to create a Factory... 
        // This is SOOOO much prettier....
            myHoles[i] = new LeakFactory().createLeak();
        }
    }
}

現在,這些新洩漏工廠中的每一個都剛剛洩漏。 你對那個怎麼想的? 這是內部類如何超過任何類型的外部類的兩個非常普遍的例子。 如果這個外部類是一個活動,想像會有多糟。

結論

這些列出了不適當使用這些物體的主要已知危險。 總的來說,這篇文章應該涵蓋了你的大部分問題,但是我知道這是一篇很糟糕的文章,所以如果你需要澄清,請告訴我。 只要你遵循上述的做法,你幾乎不會擔心洩漏。

希望這可以幫助,

FuzzicalLogic





c# design-patterns memory-leaks event-handling