5种避免C#.NET中因事件造成内存泄漏的技术
原文來自互聯網,由長沙DotNET技術社區編譯。?
5種避免C#.NET中事件造成的內存泄漏的技術
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} }優點**:簡單易讀的代碼。
缺點:您很容易忘記取消訂閱,或者在所有情況下都不會取消訂閱,這將導致內存泄漏。
注意:并非所有事件注冊都會導致內存泄漏。注冊到將要過期的事件時,不會發生內存泄漏。例如,在WPF UserControl中,您可以注冊到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/
總結
以上是生活随笔為你收集整理的5种避免C#.NET中因事件造成内存泄漏的技术的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何实时主动监控你的网站接口是否挂掉并及
- 下一篇: [GitHub] 75+的 C# 数据结