zoukankan      html  css  js  c++  java
  • [.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问题的文章:

  • 相关阅读:
    Asp.Net MVC 路由
    Http 请求处理流程
    Http Module 介绍
    彻底屏蔽鼠标右键、另存为、查看源文件
    使用TransactionScope实现单数据库连接事务操作
    Asp.Net MVC(创建一个任务列表应用程序) Part.1
    Http Handler 介绍
    jQuery.API源码深入剖析以及应用实现(1) - 核心函数篇
    安装MSSQL2008出现的问题记录
    SQL – 8.Union
  • 原文地址:https://www.cnblogs.com/Mainz/p/2173162.html
Copyright © 2011-2022 走看看