• 【.NET深呼吸】清理对象引用,有一个问题容易被忽略


    大家知道,托管代码一个重要的特点是自动管理内存,即我们常说的垃圾回收机制,那些高大上的理论我就不重复了,有兴趣的朋友可以翻书。我这个有个毛病——不喜欢很严肃地去说一些理论的东西,所以我不多介绍了。

    一般而言,当代码执行超出某个变量的有效范围后,或者不再引用某个对象实例时,该实例会发生析构,垃圾回收器很可能就要清理门户了,当然也可能不是马上清理,也许会过一会儿再清理。

    对于一些要自定义进行清理操作的类,我们会采取以下方案:

    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("
    看,析构函数调用了。
    ");
            }
    
            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

    好了,今天就扯到这里吧。

  • 相关阅读:
    25、Base64
    24、AES RSA加密处理记录
    23、获取app所占据的内存
    22、DDMS(转载)
    21、HierarchyViewer使用记录
    kubernetes 操作 serviceaccounts
    删除dashboard
    数据库建库指定UTF-8和jdbc连接字符串
    apt 被卸载
    EasyReport报表工具部署
  • 原文地址:https://www.cnblogs.com/tcjiaan/p/4280123.html
Copyright © 2020-2023  润新知