首页 > 其他随笔 > DotNet中未退订事件引起的内存泄露问题

DotNet中未退订事件引起的内存泄露问题

2009年11月12日

今天在InfoQ上看到一篇文章处理.NET中的内存泄露,里头提到说DotNet内存泄露的几种常见情况,

  • 使用静态引用
  • 未退订的事件-作者认为这是最常见的内存泄漏原因
  • 未退订的静态事件
  • 未调用Dispose方法
  • 使用不彻底的Dispose方法
  • 在Windows Forms中对BindingSource的误用
  • 未在WorkItem/CAB上调用Remove

当时对于第二点,“未退订事件”引起的内存泄露不是很理解。后来仔细分析了一下,又参考了MSDN上关于事件和委托的解释,终于意识到了自己以前没有注意的一个问题。

假设,类A提供了一个事件,类B和类C均订阅了该事件。如果类B和类C的实例不再使用了,而又没有显式退订此事件,那么类B和类C的实例将不会被GC回收,直至类A实例被销毁为止。

下面的代码说明了这种情况:

public class Container
{
    private List<Containee> _controls = new List<Containee>();
    private String _currentState;

    public event EventHandler StateChanged;

    public String CurrentState
    {
        get
        {
            return _currentState;
        }
        set
        {
            if (value != _currentState)
            {
                _currentState = value;
                if (StateChanged != null)
                {
                    StateChanged(this, null);
                }
            }
        }
    }

    public void ClearControls()
    {
        _controls.Clear();
    }
}

public class Containee
{
    private String _name;
    private Container _container;
    public Containee(String controlName, Container container)
    {
        _name = controlName;
        _container = container;
        container.StateChanged += OnStateChanged;
    }

    public void OnStateChanged(object sender, EventArgs e)
    {
        Container container = sender as Container;
        if (container != null)
        {
            // Do something....
            Console.WriteLine("{0}: State change, current state is {1}.", _name, container.CurrentState);
        }
    }
}
class Program
{
    static void Main(string[] args)
    {
        Container container = new Container();
        new Containee("Control 1", container);
        new Containee("Control 2", container);
        new Containee("Control 3", container);
        container.CurrentState = "State1";

        Console.WriteLine("================================================");
        // 清除所有子控件
        container.ClearControls();
        // 添加新控件进来
        new Containee("Control 4", container);
        new Containee("Control 5", container);
        new Containee("Control 6", container);
        container.CurrentState = "State2";
    }
}

运行结果:

image

如上述结果看到的,尽管旧控件已经被clear掉了,但是它们仍在起作用。

当我们在后面代码中将container容器上的控件清除掉的时候,我们会习惯性的以为这些控件已经不再被引用了,资源会被自动回收掉。而设计Containee的人可能在写事件订阅的时候也不会想起背后的逻辑,习以为常的觉得,只要在代码里找到该Containee实例都被“=”给了谁,然后把它的引用置为null就行了。殊不知,C#的event-delegate模型下,当一个类实例订阅了一个事件的时候,此实例已经被隐式的传给了“事件”了(CLR的事件模型是建立在委托System.Delegate之上的,事件可以看成是一个委托类)。因此,只要Container不销毁,事件也就不销毁,Containee的实例就不会被回收,除非Containee退订了该事件。

自己在写事件处理的时候根本没有考虑过这样的问题,不知道以前写过的代码会不会也存在这样的问题。不管怎么,以后得注意一些了。

——Kevin Yang

你可能对下面的文章感兴趣

本博客遵循CC协议2.5,即署名-非商业性使用-相同方式共享
写作很辛苦,转载请注明作者以及原文链接~
如果你喜欢我的文章,你可以订阅我的博客:-D点击订阅我的文章

Kevin Yang 其他随笔 , , , ,

  1. X﹏X 到现在还没有评论~
  1. 暂时没有trackbacks.