5種避免C#.NET中因事件造成記憶體洩漏的方法

語言: CN / TW / HK

原文來自網際網路,由長沙DotNET技術社群編譯。 

5種避免C #.N ET中事件造成的記憶體洩漏的技

C#(通常是.NET)中的事件註冊是記憶體洩漏的最常見原因。至少從我的經驗來看。實際上,我從事件中看到了太多的記憶體洩漏,因此 在程式碼中看到 + = 將立即使我感到懷疑。

儘管事件很常見,但它們也很危險。如果您不知道要查詢的內容,則事件很容易導致記憶體洩漏。在本文中,我將解釋此問題的根本原因,並提供幾種最佳實踐技術來解決該問題。最後,我將向您展示一個簡單的技巧,以找出您是否確實存在記憶體洩漏。

瞭解記憶體洩漏

在垃圾收集環境中,術語“記憶體洩漏”有點反直覺。當有一個垃圾收集器負責收集所有內容時,我的記憶體如何洩漏?

答案是,在存在垃圾收集器( GC )的情況下,記憶體洩漏表示有些物件仍在引用中,但實際上未被使用。由於已引用它們,因此GC將不會收集它們,並且它們將永久儲存,佔用記憶體。

讓我們來看一個例子:

public class WiFiManager

{

public event EventHandler <WifiEventArgs> WiFiSignalChanged;

// ...

}

public class MyClass

{

public MyClass(WiFiManager wiFiManager)

{

wiFiManager.WiFiSignalChanged += OnWiFiChanged;

}



private void OnWiFiChanged(object sender, WifiEventArgs e)

{

// do something

}

public void SomeOperation(WiFiManager wiFiManager)

{

var myClass = new MyClass(wiFiManager);

myClass.DoSomething();



//... myClass is not used again

}

在此示例中,我們假設 WiFiManager 在程式的整個生命週期中都處於活動狀態。執行 SomeOperation之後 ,將建立 MyClass 的例項,並且不再使用它。程式設計師可能會認為GC將收集它,但事實並非如此。所述 WiFiManager 保持在其事件MyClass的參考  WiFiSignalChanged 和它引起了記憶體洩漏。GC將永遠不會收集 MyClass

1.確保登出事件處理程式

顯而易見的解決方案(儘管並非總是最簡單的)是記住從事件中登出事件處理程式。一種方法是實現IDisposable:

public class MyClass : IDisposable

{

private readonly WiFiManager _wiFiManager;



public MyClass(WiFiManager wiFiManager)

{

_wiFiManager = wiFiManager;

_wiFiManager.WiFiSignalChanged += OnWiFiChanged;

}



public void Dispose()

{

_wiFiManager.WiFiSignalChanged -= OnWiFiChanged;

}



private void OnWiFiChanged(object sender, WifiEventArgs e)

{

// do something

}

當然,您必須確保呼叫 Dispose 。如果您有WPF控制元件,一個簡單的解決方案是退訂 Unloaded 事件。

public partial class MyUserControl : UserControl

{

public MyUserControl(WiFiManager wiFiManager)

{

InitializeComponent();

this.Loaded += (sender, args) => wiFiManager.WiFiSignalChanged += OnWiFiChanged;

this.Unloaded += (sender, args) => wiFiManager.WiFiSignalChanged -= OnWiFiChanged;

}



private void OnWiFiChanged(object sender, WifiEventArgs e)

{

// do something

}

}

優點:簡單易讀的程式碼。

缺點:您很容易忘記取消訂閱,或者在所有情況下都不會取消訂閱,這將導致記憶體洩漏。

注意:並非所有事件註冊都會導致記憶體洩漏。註冊到將要過期的事件時,不會發生記憶體洩漏。例如,在WPFUserControl中,您可以註冊到Button的Click事件。這很好,並且不需要登出,因為使用者控制元件是唯一引用該Button的控制元件。如果沒有一個人引用使用者控制元件,那麼也將沒有一個人引用按鈕,並且GC將同時收集兩者。

2.讓處理程式退訂

在某些情況下,您可能希望事件處理程式僅發生一次。在這種情況下,您將希望程式碼自己退訂。當事件處理程式是命名方法時,它很容易:

public class MyClass

{

private readonly WiFiManager _wiFiManager;



public MyClass(WiFiManager wiFiManager)

{

_wiFiManager = wiFiManager;

_wiFiManager.WiFiSignalChanged += OnWiFiChanged;

}



private void OnWiFiChanged(object sender, WifiEventArgs e)

{

// do something

_wiFiManager.WiFiSignalChanged -= OnWiFiChanged;

}

}

但是,有時您希望事件處理程式是lambda表示式。在這種情況下,以下是一種使自己退訂的有用技術:



public class MyClass

{

public MyClass(WiFiManager wiFiManager)

{

var someObject = GetSomeObject();

EventHandler<WifiEventArgs> handler = null;

handler = (sender, args) =>

{

Console.WriteLine(someObject);

wiFiManager.WiFiSignalChanged -= handler;

};

wiFiManager.WiFiSignalChanged += handler;

}

}

在上面的示例中,lambda表示式非常有用,因為您可以捕獲區域性變數 someObject ,而使用處理程式方法則無法做到這一點。

優點:簡單,易讀,只要您確定事件至少會觸發一次,就不會發生記憶體洩漏。

缺點:僅在需要處理一次事件的特殊情況下可用。

3.將弱事件與事件聚合器一起使用

在.NET中引用物件時,您基本上會告訴GC該物件正在使用中,因此請不要收集它。有一種引用物件的方法,而無需實際說“我正在使用它”。這種參考稱為

弱參考 。您是說“我不需要它,但是如果它仍然存在,那麼我會使用它”。在其他換句話說,如果某個物件僅被弱引用引用,則 GC 會收集該物件並釋放該記憶體。這是使用.NET的 WeakReference  類實現的。

我們可以通過多種方式使用它來防止記憶體洩漏。一種流行的設計模式是使用 事件聚合器 [1] 。這個概念是,任何人都可以 訂閱 T型別的事件,任何人都可以 釋出 T型別的事件。因此,當一個類釋出事件時,將呼叫所有訂閱的事件處理程式。事件聚合器使用WeakReference引用所有內容。所以即使有物體提斯 訂閱事件,仍然可以對其進行垃圾回收。

這是一個使用 Prism 流行的事件聚合器(通過NuGet  Prism.Core提供 [2] )的 示例 [3]

public class WiFiManager

{

private readonly IEventAggregator _eventAggregator;



public WiFiManager(IEventAggregator eventAggregator)

{

_eventAggregator = eventAggregator;

}



public void PublishEvent()

{

_eventAggregator.GetEvent<WiFiEvent>().Publish(new WifiEventArgs());

}

public class MyClass

{

public MyClass(IEventAggregator eventAggregator)

{

eventAggregator.GetEvent<WiFiEvent>().Subscribe(OnWiFiChanged);



}



private void OnWiFiChanged(WifiEventArgs args)

{

// do something

}

public class WiFiEvent : PubSubEvent<WifiEventArgs>

{

// ...

}

優點:防止記憶體洩漏,相對易於使用。

缺點: 充當所有事件的全域性容器。任何人都可以訂閱任何人。這使得系統在過度使用時難以理解。沒有分離的關注點。

4.對常規事件使用弱事件處理程式

藉助一些程式碼技巧,可以將弱引用與常規事件一起使用。這可以通過幾種不同的方式來實現。這是使用Paul Stovell的 WeakEventHandler [4] 的示例:

public class MyClass

{

public MyClass(WiFiManager wiFiManager)

{

wiFiManager.WiFiSignalChanged += new WeakEventHandler<WifiEventArgs>(OnWiFiChanged).Handler;

}



private void OnWiFiChanged(object sender, WifiEventArgs e)

{

// do something

}

}



public class WiFiManager

{

public event EventHandler<WifiEventArgs> WiFiSignalChanged;

// ...



public void SomeOperation(WiFiManager wiFiManager)

{

var myClass = new MyClass(wiFiManager);

myClass.DoSomething();



//... myClass is not used again

}

我真的很喜歡這種方法,因為在我們的案例中,釋出者 WiFiManager 保留了標準的C#事件。這只是這種模式的一種實現,但是實際上有很多方法可以解決。 Daniel Grunwald 寫了 一篇 [5] 有關不同實現及其差異的文章。

優點:利用標準事件。簡單。沒有記憶體洩漏。關注點分離(與事件聚合器不同)。

缺點:此模式的不同實現有一些細微之處和不同問題。該示例中的實現實際上建立了一個 註冊的 包裝 物件,該  包裝 物件從未被GC收集。其他實現可以解決此問題,但還有其他問題,例如其他樣板程式碼。在Daniel的 文章中 [6] 瞭解有關此內容的更多資訊 。

WeakReference解決方案存在的問題

使用 WeakReference 意味著 GC 將能夠在可能的情況下收集訂閱類。但是,GC不會立即收集未引用的物件。就開發商而言,它是隨機的。因此,對於弱事件,您可能會在當時不應該存在的物件中呼叫事件處理程式。

事件處理程式可能會執行無害的操作,例如更新內部狀態。或者,它可能會更改程式狀態,直到GC決定隨機收集某個時間為止。這種行為確實很危險。在 “弱事件模式是危險的”中 [7] 對此進行附加閱讀 。

5.在沒有記憶體探查器的情況下檢測記憶體洩漏

此技術是為了測試現有的記憶體洩漏,而不是編碼模式以首先避免它們。

假設您懷疑某個類存在記憶體洩漏。如果您有建立一個例項然後希望 GC 收集它的情況,則可以輕鬆地確定是否將收集您的例項或是否存在記憶體洩漏。按著這些次序:

1.將 終結器新增 到您的可疑類中,並在其中放置一個斷點:

1. 在場景 開始 時新增以下要呼叫的魔術3行:

GC.Collect();

GC.WaitForPendingFinalizers();

GC.Collect();

這將迫使GC到目前為止收集所有未引用的例項(不在生產環境中使用),因此它們不會干擾我們的除錯。

3.新增相同的3條魔術程式碼行,以 方案 之後 執行。請記住,該方案是建立並收集可疑物件的方案。

4.執行有問題的方案。

在第1步中,我告訴您在類的終結器中放置一個斷點。 第一個垃圾回收完成 之後 ,您實際上應該注意該斷點。否則,您可能會被廢棄舊例項感到困惑。需要注意的重要時刻是 您的方案 之後 偵錯程式是否在Finalizer中停止 。

它還有助於在類的建構函式中放置一個斷點。這樣,您可以計算建立次數和完成次數。如果觸發了終結器中的斷點,則GC會收集您的例項,一切正常。如果沒有,則可能發生記憶體洩漏。

這是我除錯的一種方案,該方案使用了上一種技術中的WeakEventHandler,並且沒有記憶體洩漏:

這是我使用常規事件註冊的另一種情況,它確實存在記憶體洩漏:

摘要

總是讓我感到驚訝的是,C#看起來像是一種易於學習的語言,並且提供了一個提供訓練平臺的環境。但實際上,還遠遠沒有做到。諸如使用事件之類的簡單事情,可以由未經培訓的手輕鬆地將您的應用程式變成一堆記憶體洩漏。

至於在程式碼中使用的正確模式,我認為本文的結論應該是,在所有情況下都沒有正確答案。提供的所有技術,以及他們, 視情況而定是可行的解決方案。

原來這是一個相對較大的職位,但在此問題上,我仍然處於較高水平。這恰恰證明了在這些問題上存在多少深度,以及軟體開發如何永無止境。

有關記憶體洩漏的更多資訊,請檢視我的文章 查詢,修復和避免C#.NET:8最佳實踐中的記憶體洩漏 [8] 。從我自己的經驗和其他高階.NET開發人員那裡獲得的大量資訊都為我提供了建議。它包括有關記憶體分析器,非託管程式碼的記憶體洩漏,監控記憶體等資訊。

我希望您在評論部分中留下一些反饋。並確保 訂閱 [9] 部落格並收到新帖子通知。

References

[1] 事件聚合器:  https://www.codeproject.com/Articles/812461/Event-Aggregator-Pattern

[2] Prism.Core提供:  https://www.nuget.org/packages/Prism.Core/

[3] 示例:  https://www.nuget.org/packages/Prism.Core/

[4] WeakEventHandler:  http://paulstovell.com/blog/weakevents

[5] 一篇:  https://www.codeproject.com/Articles/29922/Weak-Events-in-C

[6] 文章中:  https://www.codeproject.com/Articles/29922/Weak-Events-in-C

[7] “弱事件模式是危險的”中:  https://ladimolnar.com/2015/09/14/the-weak-event-pattern-is-dangerous/

[8] 查詢,修復和避免C#.NET:8最佳實踐中的記憶體洩漏:  https://michaelscodingspot.com/2019/01/03/find-fix-and-avoid-memory-leaks-in-c-net-8-best-practices/

[9] 訂閱:  https://michaelscodingspot.com/subscribe/