5種避免C#.NET中因事件造成記憶體洩漏的方法
原文來自網際網路,由長沙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]
事件聚合器: http://www.codeproject.com/Articles/812461/Event-Aggregator-Pattern
[2]
Prism.Core提供: http://www.nuget.org/packages/Prism.Core/
[3]
示例: http://www.nuget.org/packages/Prism.Core/
[4]
WeakEventHandler: http://paulstovell.com/blog/weakevents
[5]
一篇: http://www.codeproject.com/Articles/29922/Weak-Events-in-C
[6]
文章中: http://www.codeproject.com/Articles/29922/Weak-Events-in-C
[7]
“弱事件模式是危險的”中: http://ladimolnar.com/2015/09/14/the-weak-event-pattern-is-dangerous/
[8]
查詢,修復和避免C#.NET:8最佳實踐中的記憶體洩漏: http://michaelscodingspot.com/2019/01/03/find-fix-and-avoid-memory-leaks-in-c-net-8-best-practices/
[9]
訂閱: http://michaelscodingspot.com/subscribe/
- .NET基礎知識快速通關(9)
- .NET基礎知識快速通關(6)
- .NET基礎知識快速通關(5)
- 面試寶典之.NET基礎知識快速通關(1)
- 填坑 | .NET 在Docker中訪問MSSQL報錯
- 如何分析.NET Core HttpClient請求異常
- 理解C#泛型原理
- 萬字長文講解:什麼是「抽象」?
- 簡述使用REST API 的最佳實踐
- 如何基於.NET Core構建分散式檔案儲存系統?
- 利用SOS擴充套件庫進入高階.NET6程式的除錯
- 淺議開發者面臨的資訊偏差影響因素
- 【新書速遞】龍芯開源LoongArch版,學會造計算機!
- .NET誕生20週年 .NET 7有什麼新東西?
- 基於.NET 製作一個氣象站 IoT 應用
- [探索 .NET 6]02 比較 WebApplicationBuilder 和 Host
- [探索 .NET 6]01 揭開 ConfigurationManager 的面紗
- 基於REACT和.NET CORE整合WINDOWS身份驗證
- 驚爆:Alexa 全球排名網站即將關閉
- 面試必備之C#10語法特性總結