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

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

其他随笔

今天在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>();

	public List<Containee> Controls
	{
		get { return _controls; }
		set { _controls = value; }
	}
	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.Controls.Add(this);
		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点击订阅我的文章

  1. Jim
    | #1

    哥们,你这 _controls 就根本没有加入任何东西,后面调clear()有什么用处?代码和你所说不一致。

    • Kevin Yang
      | #2

      谢谢兄台的指正,写示例代码的时候太急,写漏了。

  1. 暂时没有trackbacks.