閱讀436 返回首頁    go 阿裏雲 go 技術社區[雲棲]


事件(Event),絕大多數內存泄漏(Memory Leak)的元凶[下篇] (提供Source Code下載)

上篇中我們談到:將一個生命周期較短的對象(對象A)注冊到一個生命周期較長(對象B)的某個事件(Event)上,兩者便無形之間建立一個引用關係(B引用A)。這種引用關係導致GC在進行垃圾回收的時候不會將A是為垃圾對象,最終使其常駐內存(或者說將A捆綁到B上,具有了和B一樣的生命周期)。這種讓無用的對象不能被GC垃圾回收的現象,在托管環境下就是一種典型的內存泄漏問題。我們今天將會著重解釋其背後的原因。[本篇文章的Source Code從這裏下載)

一、CLR垃圾回收簡介

在一個托管應用程序中,我們通過不同的方式創建一個托管對象(比如通過new關鍵字、反射或反序列化等)時,CLR會在托管堆為該對象開辟一塊內存空間。對象的本質就是存儲於某塊內存中數據的體現,對象的生命周期終止於相應內存被回收之時。對於CLR來說,負責對托管堆(在這裏主要指GC堆)進行回收的組件是垃圾收集器(GC),GC掌握著托管對象的生殺大權,決定著托管對象的生命周期。

當GC在進行垃圾回收的時候,會將“無用”的對象標記為垃圾對象,然後再對垃圾對象進行清理。GC對“無用”對象的識別機製很簡單:判斷對象是否被“根(Root)”所引用。在這裏,“根”是對一組當前正被使用,或者以後可能被使用的對象的統稱,大體包括這樣的對象:類型的靜態字段或當前的方法參數和局部變量、CPU寄存器等。

所以,孤立存在的對象將難逃被GC回收的厄運。反之,如果希望某個對象常駐內存中,我們唯一的方式就是通過某個“根”引用該對象。如果想讓對象實例按照我們希望的方式創建、存活和消亡,所以我們唯一的方式也隻能是:在希望它存活的時候讓它被某個“根”引用,從而阻止GC將其回收;在希望它被回收的時候連“根”去除,使GC能夠將其回收。

二、關於事件(Event)那點事

簡單介紹了CLR的垃圾回收機製,我們再來談談關於事件的話題。我們知道,事件本質上就是一個System.Delegate對象。Delegate是一個特別的對象,我們單從語意上去來理解Delegate:Delegate的中文翻譯是“代理”,意思是委托某人做某事。比如說,我請某人作為我們的代理律師打官司,就是一個很好的Delegate的例子。仔細分析我舉的這個例子,我們可以將一個Delegate分解成兩個部分:委托的事情(打官司)和委托的對象(某個律師)。與之相似地,.NET的Delegate對象同樣可以分解成兩個部分:委托的功能(Method)和目標對象(Target),這可以直接從Delegate的定義就可以看出來:

   1:  
   2: public abstract class Delegate : ICloneable, ISerializable
   3: {
   4:     // Others
   5:     public MethodInfo Method { get; }
   6:     public object Target { get; }
   7: }

我們最常用的兩個事件處理類型EventHandlerEventHandler<TEventArgs>本質上就是一個Delegate,下麵是它們的定義。但是並不是直接繼承自System.Delegate,而是繼承自System.MulticastDelegateMulticastDelegate派生於DelegateMulticastDelegate不涉及到本篇文章的主題,在這裏就不再贅言介紹了。

   1: [Serializable, ComVisible(true)]
   2: public delegate void EventHandler(object sender, EventArgs e);
   3:  
   4: [Serializable]
   5: public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e) where TEventArgs: EventArgs;

回到我們上篇關於TodoList的例子,在TodoListForm加載的時候,注冊了單例對象TodolistManager的TodoListChanged事件,相關的代碼如下

   1: private void TodoListForm_Load(object sender, EventArgs e)
   2: {
   3:     SynchronizationContext = SynchronizationContext.Current;
   4:     TodoListManager.Instance.TodoListChanged += TodoListManager_TodoListChanged;    
   5: } 
   6:  
   7: private void TodoListManager_TodoListChanged(object sender, TodoListEventArgs e)
   8: {
   9:     SynchronizationContext.Post(
  10:         state =>
  11:         {
  12:             BindingSource bindingSource = new BindingSource();
  13:             bindingSource.DataSource = e.TodoList;
  14:             this.dataGridViewTodoList.DataSource = bindingSource;
  15:         }, null);
  16: }

image 經過簡單一句事件注冊代碼就通過一個EventHandler(在本例中具體類型為EventHandler<TodoListArgs>)事件的源(Source,即TodoListManager)和事件的監聽者(即TodoListForm)兩著關聯起來,三者之間的關係如右圖(點擊圖片看大圖)所示。從這張圖中我們可以看到:TodoListForm實際上是通過注冊的EventHandler的Target屬性被TodoListManager間接引用著的。所以才會導致TodoListForm在關閉之後,永遠不能拿成為垃圾對象,因為TodoListManager是一個基於static屬性定義的Singleton對象,永遠是GC的根。

 

三、有什麼方式能夠更好的解決這個問題嗎?

上麵的這個問題可以簡單地通過在某些時機解除事件的注冊的方式來解決,所以很多人認為這是由不好的編程習慣造成的,不應該是一個問題。不錯,作為一個優秀的編程人員,在編寫事件注冊的時候應該具一種意識:是否應該在某個時機解除該事件的注冊。但是,再強的老虎也有打盹的時候,況且我們麵對的開發人員也許沒有你想的那麼優秀。此外,作為一個架構師或者是框架的設計者,是否應該考慮提高你應用的容錯能力呢?我的意思是:既然這是一個大家普遍會犯的毛病,那麼你應該考慮提高你程序的健壯性以容忍開發人員犯這種“大眾性的錯誤”。

image 如何來更好地解決這個問題呢?實際上我們的目的很單純:當對象A注冊到B的某個事件上,A並不受到B的“強製引用”。我想說道這裏,有些讀者應該心理有了答案:既然不能“強引用(Strong Reference)”,那就隻能是“弱引用(Weak Reference)”。不錯,我們就是通過System.WeakReference來解決這個問題。具體來講,我們需要采取某種機製,讓事件源(Event Source)的EventHandler通過WeakReference的方式與事件監聽者建立關係。隻有在這種情況下,事件監聽者沒有了事件源的強製引用,在我們不用的時候才能及時成為垃圾對象,等待GC對它的清理。右圖(點擊圖片看大圖)很好的揭示了這種解決方案的本質。

我們具體的做法其實並不複雜,僅僅是寫了如下一個特殊的WeakReferenceHandler對現有的EventHandler<TEventArgs>進行了改造,下麵是實現WeakReferenceHandler的所有代碼。我們通過傳入EventHandler<TEventArgs>對象構造WeakReferenceHandler,在EventHandler<TEventArgs>的Target屬性基礎上建立WeakReference對象,在執行處理事件的時候通過該WeakReference找到真正的目標對象,如果找得到則通過反射在其基礎上調用相應的方法;反之,如果通過不能得到Target,那麼表明該事件的監聽對象已經被GC當作垃圾對象回收掉了。為了在注冊事件的時候方遍,特定義了一個隱式的類型轉換:WeakReferenceHandler轉換成EventHandler<TEventArgs>

   1: using System;
   2: using System.Reflection;
   3: namespace Artech.MemLeakByEvents
   4: {
   5:     public class WeakEventHandler<TEventArgs> where TEventArgs : EventArgs
   6:     {
   7:         public WeakReference Reference
   8:         { get; private set; }
   9:  
  10:         public MethodInfo Method
  11:         { get; private set; }
  12:  
  13:         public EventHandler<TEventArgs> Handler
  14:          { get; private set; }
  15:  
  16:         public WeakEventHandler(EventHandler<TEventArgs> eventHandler)
  17:         {
  18:             Reference = new WeakReference(eventHandler.Target);
  19:             Method = eventHandler.Method;
  20:             Handler = Invoke;
  21:         }
  22:  
  23:         public void Invoke(object sender, TEventArgs e)
  24:         {
  25:             object target = Reference.Target;
  26:             if (null != target)
  27:             {
  28:                 Method.Invoke(target, new object[] { sender, e });
  29:             }
  30:         }
  31:  
  32:         public static implicit operator EventHandler<TEventArgs>(WeakEventHandler<TEventArgs> weakHandler)
  33:         {
  34:             return weakHandler.Handler;
  35:         }
  36:     }
  37: }

那麼在實際進行事件注冊的時候,你就可以采用下麵的方式了,照樣很簡單,對不對?

   1: private void TodoListForm_Load(object sender, EventArgs e)
   2: {   
   3:     TodoListManager.Instance.TodoListChanged += new WeakEventHandler<TodoListEventArgs>(TodoListManager_TodoListChanged);
   4: }

不過,任何事情都有其兩麵性,很難同時兼顧(比如軟件架構下的Performance和Scalability),基於上麵這種解決方式雖然能夠有效地解決由於事件注冊導致的內存泄露問題,但是會帶來一定的性能損失,畢竟原來直接的事件注冊方式是一種“強類型的Delegate”,具有更好的執行性能。關於本篇文章提供的實現方式,基本上借鑒了這篇文章:《[轉]如何解決事件導致的Memory Leak問題:Weak Event Handlers》,有興趣的朋友不妨認真讀讀。


作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-30 11:04:16

  上一篇:go  擴展UltraGrid控件實現對所有數據行的全選功能[Source Code下載]
  下一篇:go  WCF版的PetShop之三:實現分布式的Membership和上下文傳遞