大家知道,托管代码一个重要的特点是自动管理内存,即我们常说的垃圾回收机制,那些高大上的理论我就不重复了,有兴趣的朋友可以翻书。我这个有个毛病——不喜欢很严肃地去说一些理论的东西,所以我不多介绍了。
一般而言,当代码执行超出某个变量的有效范围后,或者不再引用某个对象实例时,该实例会发生析构,垃圾回收器很可能就要清理门户了,当然也可能不是马上清理,也许会过一会儿再清理。
对于一些要自定义进行清理操作的类,我们会采取以下方案:
1、写上析构函数,在析构函数中清理。
2、实现IDisposable接口,并实现Dispose方法,在方法中编写自定义清理代码。当该类型被实例化后,最后不再使用时会调用Dispose方法清理,如果顺利清理,最后还会调用类型的析构函数。通常,如何实现了IDisposable接口,就不必再写上析构函数了。如果希望Dispose方法被自动调用,可以在实例化对象的代码包装在using语句块中,当执行完using块时会自动调用Dispose方法。
可能有人笑了,老周,你太逗了,这些基础知识谁不知道?当然,我说上面那些内容是为了绕个小圈子,以便进入主题。于是,我产生了一个疑问:是不是存在某些情景下,可能导致对象实例不会被回收呢?就算你调用了Dispose方法,就算你把变量设为null来解除引用,就算你调用GC类的方法来回收……
经过老周测试,还真有这种情况,而且很多朋友都很有可能会忽略,甚至在意识认知上误认为对象实例已经被回收,而实际上是没有回收的。
我简单说一下这种情形:
比如有一个静态类(静态类的成员必是静态的)A,里面有静态事件。随后在其他类的实例中处理A类的静态事件,并且处理事件的方法就位于这个实例对象上……
不急,我们还是看真实的例子吧。假如我定义了一个静态类MyChecker,它里面有个静态事件CheckEvent。
public static class MyChecker { #region 静态事件 public static event EventHandler CheckEvent; #endregion public static void CallEvent() { if (CheckEvent != null) { CheckEvent(new object(), EventArgs.Empty); } } }
只要CallEvent方法被调用,CheckEvent事件会被引发。
然后,定义另一个类SampleClass,并在该类中处理刚才MyChecker中的静态事件。
class SampleClass:IDisposable { public SampleClass() { MyChecker.CheckEvent += MyChecker_CheckEvent; } void MyChecker_CheckEvent(object sender, EventArgs e) { new Form2().Show(); } ~SampleClass() { System.Diagnostics.Debug.WriteLine("\n看,析构函数调用了。\n"); } public void Dispose() { //…… } }
在类的构造函数中,附加CheckEvent事件的处理,处理方法名为MyChecker_CheckEvent。
可能大家已经发现,老周写的SampleClass类有点恐怖气息,既实现了Dispose方法,怎么又写了析构函数,我这里写上析构函数是为了验证类的实例是否真的被清理,如果实例真的被回收,那么Debug类会在“输出”窗口中输出提示,如果没有提示输出,说明类的实例还霸占着内存。
接下来测试一下。
SampleClass sc = new SampleClass(); await Task.Delay(10 * 1000); sc.Dispose(); sc = null; GC.Collect();
实例化SampleClass后,然后Delay会暂停10秒,10秒钟过后会调用Dispose方法,并设置变量为null引用,我害怕不能及时清理,连GC.Collect方法也用上了。
而在等待这10秒期间,可以调用静态类的CallEvent方法来引发静态事件CheckEvent。
MyChecker.CallEvent();
按照一般理解,在10秒钟后,SampleClass实例应该被清理,并且在“输出”窗口会输出提示。
好,现在试一下。
……
实验结果表明,输出 窗口中连鸭毛都没有输出,这说明10秒钟后,SampleClass实例根本没有发生析构。于是又出问题了,这是怎么回事?SampleClass实例不是不存在引用了吗,怎么不发生析构?
其实我们忽略了一点:静态事件CheckEvent还跟SampleClass实例的方法绑定着呢,实质上,虽然将变量设为null,可是SampleClass实例中的MyChecker_CheckEvent方法还被静态类中的静态事件引用着,所以不会被回收。不知道你明白了没。
这个问题很多朋友在实际开发中都会忽略,还得意地以为Sample实例真的被回收了,实际上实例不会被回收,除非程序结束。因为MyChecker是静态类,不基于实例。如果MyChecker不是静态类,那么当MyChecker的实例释放后,SampleClass实例就可以被释放了。
那么,如何解决呢?很简单,只要在SampleClass类的Dispose方法中解除静态事件与方法的绑定即可,这样的话,静态事件就不再引用实例中的方法成员了,此时实例就可以发生析构了。
public void Dispose() { MyChecker.CheckEvent -= MyChecker_CheckEvent; }
这个例子研究,告诉我们:在类实例中处理静态事件时一定要小心。
本示例的源码下载:http://files.cnblogs.com/files/tcjiaan/refsample.zip
好了,今天就扯到这里吧。