[.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: }
运行结果:
没有手动注销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: }
这样就内存泄露了,看看运行结果:
加个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程序内存泄露>以及<如何写高性能的托管程序>
- How to detect and avoid memory and resources leaks in .NET applications
- Writing High-Performance Managed Applications : A Primer
有关.NET的自动内存管理机制、GC机制,垃圾回收原理等深层次内容,请仔细阅读下面的内容:
- 买书《CLR Via C#(3rd Edition)》,里面有《Memory Management》这一章专门讲述了.NET CLR的自动内存管理和垃圾回收机制
- CodeProject上的文章《Memory Management Misconceptions》有助你深入理解Root, Generation 0, 1…
参考:园子里Artech关于Event handler问题的文章: