未释放事件Handler可能导致内存泄漏

以前曾看见过这样一个问题:托管代码会不会导致内存泄漏。自己对GC的了解也不是很深,但还是比较赞成这样的观点:托管代码不会产生内存泄漏,除非你没有正确释放非托管资源。
今天看到一个非常有趣的例子,关于没有释放事件的Handler导致的内存泄漏。
以前对于释放Handler的观念是一点也没有,这主要因为没此方面的意识,没有养成好的习惯。只知道当关心这个事件的时候就注册一下, 暂时不关心了就移除掉。却从来没有想到最终不移除不必要的Handler会导致此类无法被正常回收,导致不必要的内存浪费。

事情是这样的,今天在看项目Source Code的时候发现一个有趣的字眼:"WeakEvent". 自己以前对WeakReference有点了解,所以就好奇地看看这是个啥玩意。
发现其是一种通过弱引用实现的Delegate。因为没有太多的注释,所有不知其为啥用此种方式来封装事件。于是顺手Google了一下,找到了一篇关于weak event的非常有意思的文章。
文章里提出了一个问题,场景如下:


using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Win32;

namespace ConsoleApplication16
{
    
class DisplaySettingsListener
    
{
        
byte[] m_ExtraMemory = new byte[1000000];

        
public DisplaySettingsListener()
        
{
            SystemEvents.DisplaySettingsChanged 
+= new EventHandler(ehDisplaySettingsChanged);
        }


        
private void ehDisplaySettingsChanged(object sender, EventArgs e)
        
{
        }

    }


    
class Program
    
{
        
static void DisplayMemory()
        
{
            Console.WriteLine(
"Total memory: {0:###,###,###,##0} bytes", GC.GetTotalMemory(true));
        }


        
static void Main()
        
{
            DisplayMemory();
            Console.WriteLine();
            
for (int i = 0; i < 5; i++)
            
{
                Console.WriteLine(
"--- New Listener #{0} ---", i + 1);
                DisplaySettingsListener listener 
= new DisplaySettingsListener();
                listener 
= null;
                GC.Collect();

                DisplayMemory();
            }

            Console.Read();
        }


    }

}

运行的结果如下:

 image

虽然我们释放了对listener的引用,并且强制GC进行回收,但我们可以看到其内存占用量还是变大了,出乎了我的意料。
这就是该文作者指出的事件列表里保存的是一个强引用而非弱引用。虽然上面释放了listener变量对Listener实例的引用,但因为仍然在DisplaySettingsChanged事件列表里保存了对Listener实例的引用,导致Listener实例并不能被垃圾回收(有人引用,自然不会回收)。
那么接下来看看下面的代码:


    class DisplaySettingsListener : IDisposable
    
{
        
byte[] m_ExtraMemory = new byte[1000000];

        
public DisplaySettingsListener()
        
{
            SystemEvents.DisplaySettingsChanged 
+= new EventHandler(ehDisplaySettingsChanged);
        }


        
private void ehDisplaySettingsChanged(object sender, EventArgs e)
        
{
        }


        
#region IDisposable Members

        
public void Dispose()
        
{
            SystemEvents.DisplaySettingsChanged 
-= new EventHandler(ehDisplaySettingsChanged);
        }


        
#endregion

    }


    
class Program
    
{
        
static void DisplayMemory()
        
{
            Console.WriteLine(
"Total memory: {0:###,###,###,##0} bytes", GC.GetTotalMemory(true));
        }


        
static void Main()
        
{
            DisplayMemory();
            Console.WriteLine();
            
for (int i = 0; i < 5; i++)
            
{
                Console.WriteLine(
"--- New Listener #{0} ---", i + 1);
                DisplaySettingsListener listener 
= new DisplaySettingsListener();
                listener.Dispose();
                listener 
= null;
                GC.Collect();

                DisplayMemory();
            }

            Console.Read();
        }


    }

运行结果如下:

image