[.NET]Event handler没有注销就会内存泄露吗?

我们经常会写EventHandler += AFunction; 如果没有手动注销这个Event handler类似:EventHandler –= AFunction 会内存泄露吗?会! 这个Event handler的问题是内存泄露的几大元凶之一。通常观察者模式就是用EventHandler来实现,也容易内存泄露。话虽如此,但要分清楚几个情况,不要谈虎色变。其实大多数有时候我们没必要手动注销EventHandler,也没有内存泄露。看下面的例子:

 

没有手动注销Event handler也没有内存泄露

   1: class TestClassHasEvent
   2: {
   3:     public delegate void TestEventHandler(object sender, EventArgs e);
   4:     public event TestEventHandler YourEvent;
   5:     protected void OnYourEvent(EventArgs e)
   6:     {
   7:         if (YourEvent != null) YourEvent(this, e);
   8:     }
   9: }
  10:  
  11: class TestListener 
  12: {
  13:     byte[] m_ExtraMemory = new byte[1000000];
  14:  
  15:     private TestClassHasEvent _inject;
  16:  
  17:     public TestListener(TestClassHasEvent inject)
  18:     {
  19:         _inject = inject;
  20:         _inject.YourEvent += new TestClassHasEvent.TestEventHandler(_inject_YourEvent);
  21:     }
  22:     
  23:     void _inject_YourEvent(object sender, EventArgs e)
  24:     {
  25:         
  26:     }
  27: }
  28:  
  29: class Program
  30: {
  31:     static void DisplayMemory()
  32:     {
  33:         Console.WriteLine("Total memory: {0:###,###,###,##0} bytes", GC.GetTotalMemory(true));
  34:     }
  35:  
  36:     static void Main()
  37:     {
  38:         DisplayMemory();
  39:         Console.WriteLine();
  40:         for (int i = 0; i < 5; i++)
  41:         {
  42:             Console.WriteLine("--- New Listener #{0} ---", i + 1);
  43:  
  44:             var listener = new TestListener(new TestClassHasEvent());
  45:             ////listener = null; //可有可无
  46:             
  47:             GC.Collect();
  48:             GC.WaitForPendingFinalizers();
  49:             GC.Collect();
  50:             DisplayMemory();
  51:             
  52:         }
  53:         Console.Read();
  54:     }
  55: }    

运行结果:

image

 

没有手动注销Event handler内存泄露

上面的例子没有注销事件,也没有内存泄露。我们来改一行代码,就内存泄露了:

把下面这段:

   1: public TestListener(TestClassHasEvent inject)
   2: {
   3:     _inject = inject;
   4:     _inject.YourEvent += new TestClassHasEvent.TestEventHandler(_inject_YourEvent);
   5: }

改成:

   1: public TestListener(TestClassHasEvent inject)
   2: {
   3:     SystemEvents.DisplaySettingsChanged += new EventHandler(SystemEvents_DisplaySettingsChanged);
   4: }
   5:  
   6: void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e)
   7: {
   8:   
   9: }

这样就内存泄露了,看看运行结果:

image

加个Dispose手动注销事件,然后使用Using关键字,就没有问题了,不会内存泄露了:

   1:  
   2:  
   3: class TestListener : IDisposable
   4: {
   5:     byte[] m_ExtraMemory = new byte[1000000];
   6:  
   7:     private TestClassHasEvent _inject;
   8:  
   9:     public TestListener(TestClassHasEvent inject)
  10:     {
  11:         SystemEvents.DisplaySettingsChanged += new EventHandler(SystemEvents_DisplaySettingsChanged);
  12:     }
  13:  
  14:     void SystemEvents_DisplaySettingsChanged(object sender, EventArgs e)
  15:     {
  16:       
  17:     }
  18:     
  19:     #region IDisposable Members
  20:  
  21:     public void Dispose()
  22:     {
  23:         SystemEvents.DisplaySettingsChanged -= new EventHandler(SystemEvents_DisplaySettingsChanged);
  24:     }
  25:  
  26:     #endregion
  27: }
  28:  
  29: class Program
  30: {
  31:     static void DisplayMemory()
  32:     {
  33:         Console.WriteLine("Total memory: {0:###,###,###,##0} bytes", GC.GetTotalMemory(true));
  34:     }
  35:  
  36:     static void Main()
  37:     {
  38:         DisplayMemory();
  39:         Console.WriteLine();
  40:         for (int i = 0; i < 5; i++)
  41:         {
  42:             Console.WriteLine("--- New Listener #{0} ---", i + 1);
  43:             
  44:             using (var listener = new TestListener(new TestClassHasEvent()))
  45:             {
  46:                 //do something
  47:             }
  48:             GC.Collect();
  49:             GC.WaitForPendingFinalizers();
  50:             GC.Collect();
  51:             DisplayMemory();
  52:             
  53:         }
  54:         Console.Read();
  55:     }
  56: }

 

上面两个例子一个内存泄露,一个没有内存泄露,我想你应该知道原因了,根本区别在于后者有个SystemEvents.DisplaySettingsChanged事件,来看看这个静态Static事件的定义:

   1: // Type: Microsoft.Win32.SystemEvents
   2: // Assembly: System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
   3: // Assembly location: C:\Program Files\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.0\Profile\Client\System.dll
   4:  
   5: using System;
   6: using System.ComponentModel;
   7:  
   8: namespace Microsoft.Win32
   9: {
  10:     public sealed class SystemEvents
  11:     {
  12:         public static IntPtr CreateTimer(int interval);
  13:         public static void InvokeOnEventsThread(Delegate method);
  14:         public static void KillTimer(IntPtr timerId);
  15:         public static event EventHandler DisplaySettingsChanging;
  16:         public static event EventHandler DisplaySettingsChanged;
  17:         public static event EventHandler EventsThreadShutdown;
  18:         public static event EventHandler InstalledFontsChanged;
  19:  
  20:         [EditorBrowsable(EditorBrowsableState.Never)]
  21:         [Obsolete("This event has been deprecated. http://go.microsoft.com/fwlink/?linkid=14202")]
  22:         [Browsable(false)]
  23:         public static event EventHandler LowMemory;
  24:  
  25:         public static event EventHandler PaletteChanged;
  26:         public static event PowerModeChangedEventHandler PowerModeChanged;
  27:         public static event SessionEndedEventHandler SessionEnded;
  28:         public static event SessionEndingEventHandler SessionEnding;
  29:         public static event SessionSwitchEventHandler SessionSwitch;
  30:         public static event EventHandler TimeChanged;
  31:         public static event TimerElapsedEventHandler TimerElapsed;
  32:         public static event UserPreferenceChangedEventHandler UserPreferenceChanged;
  33:         public static event UserPreferenceChangingEventHandler UserPreferenceChanging;
  34:     }
  35: }

 

注意Static,注意Singleton

这种static的东西生命周期很长,永远不会被GC回收,一旦被他给引用上了,那就不可能释放了。上面的例子就是SystemEvents.DisplaySettingsChanged += new EventHandler(SystemEvents_DisplaySettingsChanged);那就意味着这个类被SystemEvents.DisplaySettingsChanged 引用了,通过它的函数。另外一个要注意的是Singleton单例模式实现的类,他们也是static的生命周期很长,要注意引用链,你的类是否被它引用上,如果在它的引用链上,就内存泄露了。

 

注意永远不释放的东西

还有一种情况,既不是你的对象被static对象而不能释放,也不是Singleton,而是你的对象被一个永远不释放的对象引用着,这个对象或许不是static的。这种类型很多,比如你的界面有个MainForm,嘿嘿,这个MainForm永远不会关闭和释放的,被它引用了那就不会释放了。看个例子:

MainForm里面有个public event,MainForm里面打开Form2,然后关闭,看看Form2能不能释放:

   1: public partial class MainForm : Form
   2: {
   3:     public event PropertyChangedEventHandler PropertyChanged;
   4:  
   5:     protected virtual void OnPropertyChanged(string propertyName)
   6:     {
   7:         PropertyChangedEventHandler handler = PropertyChanged;
   8:  
   9:         if (handler != null)
  10:             handler(this, new PropertyChangedEventArgs(propertyName));
  11:     }
  12:  
  13:     public MainForm()
  14:     {
  15:         InitializeComponent();
  16:     }
  17:  
  18:     private void button1_Click(object sender, EventArgs e)
  19:     {
  20:         Form2 frm = new Form2();
  21:  
  22:         this.PropertyChanged += frm.frm_PropertyChanged; 
  23:         //MainForm referenced form2, because main form is not released, therefore form2 will not released.
  24:  
  25:         DialogResult d = frm.ShowDialog();
  26:         
  27:         GC.Collect();
  28:         ShowTotalMemory();
  29:  
  30:     }
  31:  
  32:     
  33:  
  34:     private void ShowTotalMemory()
  35:     {
  36:         this.listBox1.Items.Add(string.Format("Memory: {0:###,###,###,##0} bytes", GC.GetTotalMemory(true)));
  37:     }
  38: }

Form2里面有个函数:

   1: public partial class Form2 : Form
   2: {
   3:     public Form2()
   4:     {
   5:         InitializeComponent();
   6:     }
   7:     public void frm_PropertyChanged(object sender, PropertyChangedEventArgs e)
   8:     {
   9:  
  10:     }
  11: }

所以这种情况下,你的Event handler没有手动注销,那就肯定内存泄露了。

 

WeakReference就能解决问题吗?

和强引用对应的,有个弱引用(WeakReference),和Event handler对应的还有WeakEventHdnler。甚至还有WeakPropertyChangedListener。都是号称可以一劳永逸的东东,也就是说用了这些东西,内存肯定不会泄露,即使你没有手动去注销Event handler。看起来似乎问题解决了。几个问题:

  • WeakReference和强引用在创建和调用的时候有几十倍的性能差(40-50倍),而且WeakRefence有专门的对象管理器来贮存这些弱引用对象,扫描它们是否还用得着,维护这个列表也需要开销。
  • 引用链上的情况很复杂:假设A弱引用b,而从b –> c –> d这条引用链条上有内存泄露,还是一样,内存泄露。也就是说:b不会释放,尽管它是弱引用的。

 

深入思考和继续阅读

通常.NET程序的内存泄露原因:

  • Static references
  • Event with missing unsubscription
  • Static event with missing unsubscription
  • Dispose method not invoked
  • Incomplete Dispose method

有关如何避免.NET程序的内存泄露,请仔细阅读MSDN这两篇文章,详细讲述了<如何检测.NET程序内存泄露>以及<如何写高性能的托管程序>

有关.NET的自动内存管理机制、GC机制,垃圾回收原理等深层次内容,请仔细阅读下面的内容:

参考:园子里Artech关于Event handler问题的文章:

posted on 2011-09-10 14:20  Mainz  阅读(2514)  评论(0编辑  收藏  举报

导航