其实标题有误,应该是“为什么Winform的Timer启动以后,不会被垃圾回收”。
该贴是分享贴,将通过一步步的试验,来解释Timer不被回收的原因。
一、垃圾回收的条件
我们知道,DotNet的垃圾回收很聪明,它将回收不再使用的对象。这里的‘不再使用’的具体逻辑,可以参考微软的文档,但简单的说,条件就是‘没有人引用’。
现在,创建一个WinForm的项目(不是Web或WPF),并拖一个按钮到设计器窗口上,在设计器窗口上双击按钮以生成如下代码,作为试验的起点:
using System;
using System.Windows.Forms;
using System.Reflection;
namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
}
}
}
当GC回收一个对象时,如果该对象有终结函数(Finerlizer),那么GC将调用终结函数。
因此,我们可以写一个终结函数来观察对象回收。
现在,在Form1后面,添加一个MyTimer的类,它将继承System.Windows.Forms.Timer。其中的~MyTimer()就是终结函数得写法。
public class MyTimer : System.Windows.Forms.Timer
{
~MyTimer()
{
MessageBox.Show("MyTimer 销毁了");
}
}
在InitializeComponent();后面,创建一个‘临时’的MyTimer实例:
public Form1()
{
InitializeComponent();
System.Windows.Forms.Timer timer = new MyTimer() { Interval = 1000 };
timer.Tick += delegate { this.Text = DateTime.Now.Second.ToString(); };
}
现在,我们在button1_Click中添加代码,来提早启动垃圾回收,看看是否timer实例被回收了:
private void button1_Click(object sender, EventArgs e)
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
编译项目并运行,按下Button按钮,应该可以观察到一个弹出消息框,说"MyTimer 销毁了"。
这里的timer没有被除了Form1()函数体外的任何人引用。因此,它就属于‘不再使用’的对象,就可以在垃圾回收中被清除。
二、启动后的Time不被回收
现在,在timer.Tick += ...这行的后面启动定时器
timer.Tick += delegate { this.Text = DateTime.Now.Second.ToString(); };
timer.Start();
编译运行,按下Button按钮,以观察到定时器促使窗口标题每秒变动,但没有销毁消息框弹出。
疑问是,timer还是本地变量,timer还是没有任何人引用,为什么它不会被垃圾回收呢?
答案是,微软故意阻止了timer被回收,具体做法是采用了GCHandle.Alloc来保护对象不被回收[备注1]。
现在,把timer.Start()注释掉,加上这么一句:
//timer.Start();
System.Runtime.InteropServices.GCHandle.Alloc(timer);
编译运行,点击按钮,看不到销毁消息框弹出。.GCHandle.Alloc起到了阻止垃圾回收的作用。
这里不讨论微软为什么要阻止timer被回收。但如果阅读微软的源代码,你会发现在Timer.Enable属性下,启用Timer的时候,用了GCHandle.Alloc,禁用的时候用了GCHandle.Free。[备注2]
而启动定时器,内部调用了Timer.Enable=true。
if (value)
{
if (timerWindow == null)
{
timerWindow = new TimerNativeWindow(this);
}
timerRoot = GCHandle.Alloc(this);
timerWindow.StartTimer(interval);
}
else
{
if (timerWindow != null)
{
timerWindow.StopTimer();
}
if (timerRoot.IsAllocated)
{
timerRoot.Free();
}
}
三、Timer导致的内存‘泄露’
由于运行中的Winform定时器不会被垃圾回收,它Tick事件下登记的回调函数也不会被回收。
如果回调函数是成员函数(相对于静态函数),那么该成员归属的对象,也不会被回收。这就造成了内存泄露。
public class A
{
string _name;
public A(string name)
{
_name = name;
Timer timer = new Timer() { Interval = 10000000 };
timer.Tick += delegate { MessageBox.Show(_name); };
timer.Start();
}
}
void Test()
{
A a = new A("实例a");
a = new A("实例b");
a = null; // 两个实例都不会得到回收,造成内存泄露
}
四、结论
Winform定时器的生命周期应该妥善管理。由于它是Component,实现了IDisposable,我们应该利用IDisposable来避免内存的泄露。即,当我们不用定时器时,要显式地停止它或销毁它。这里可以使用timer.Stop();或者timer.Dispose。
WPF下的DispatherTimer也有同样的问题,只不过它是被Dispatcher.CurrentDispatcher所引用,从而不被回收。
备注1: (GCHandle.Alloc 方法)
http://msdn.microsoft.com/zh-cn/library/vstudio/a95009h1%28v=vs.100%29.aspx
备注2: (Timer.cs源代码)
http://referencesource.microsoft.com/#System.Windows.Forms/ndp/fx/src/winforms/Managed/System/WinForms/Timer.cs