• 由于闭包引起的内存泄漏


      以前写的, 从其它地方搬运过来, 因为这些东西对项目的影响越来越大, 日积月累的会成为拖累项目的又一座大山...

      在C#中经常使用闭包或lambda对对象进行引用的时候, 需要非常小心, 因为被引用对象在任何被销毁的情况下, 在被闭包引用之后都是无法释放的, 这里的对象指的是一般对象以及资源对象.

      首先看看一个一般对象, 继承于MonoBehaviour:

     

    在测试代码中进行引用:

    这种情况下, Log会一直打出来, 进行DEBUG断点查看:

    可以看到, ClosureTest对象的基类对象被设置成了一个 "null" 对象并且所有成员变量或函数都成为无法操作的了:

      ClosureTest类的ABC, DDD 对象却还是存在的, Log就会一直打出来. 这就是对象引用造成的内存泄漏, 可以说Unity底层只对它的基类负责, 如果用户继承了基类, 那么就需要在OnDestroy函数中进行释放. 这种情况的明显表现就是在游戏发包以后用户错误日志上传到后台, 收到大量的空对象错误信息, 就是这样来的, 因为在判断对象是否被销毁需要用

    if(closureTest) 而不是 if(closureTest != null) 这样错误判断非空然后操作基类对象, 就报错了.

      说回内存泄漏, 在这种情况下只能通过设置 _tick = null 才能解除引用, 最后才能释放对象. 至于资源对象也一样, 如果上面的 ClosureTest 中有某资源比如Texture2D的引用, 那么即使调用Resources.UnloadUnusedAssets();同样无法释放资源. 资源引用的问题很多时候也出现在异步资源的加载过程中, 很多人会将加载回调封装在一个加载对象中, 比如下面这样:

    MyResLoader.LoadAsync<Texture2D>("pic1", (_tex2D)=>{}); 

     这样的形式是比较常见的, 在底层的回调或是封装中对输入的Action进行二次封装造成临时对象的闭包也是可能的:

    LoadAsync<T>(string loadPath, System.Action<T> call)
    
    {
    
        var tex = ...
    
        var call = new System.Action(()=>{call(tex);});    // 比如是这样的封装形成的闭包
    
        ......
    
    }

      所以需要注意这些回调能正确被清空. 内存泄漏在资源的时候比较容易查看出来, 因为资源数量有限而且有Profiler, 普通类对象就需要靠自己了.

    补充 : 异步回调传入的函数, 比如上文的

     MyResLoader.LoadAsync<Texture2D>("pic1", (_tex2D)=>{ Debug.Log(_tex2D); });

      这个函数不管是Lambda 还是Action或是delegate亦或是函数, 即使在调用之后清空了, 或是运行完了整个scope也就是生命周期了, 结果都是一样的, 都需要在一次GC之后才会回收这个对象然后才能解除对Texture2D这个对象的引用, 在这里可以把回调函数看成一个类对象, 它里面引用了你的Texture2D, 当你把这个回调置空之后, 它的引用仍然在, 只能在下次GC之后才能被解除. 当引用还在的时候你再次加载资源, 那么资源就在内存中重复了! 然后这个没有被任何人引用的资源, 就成了野资源, 需要等待下一次

    Resources.UnloadUnusedAssets();

      才能把它清除掉.

    补充2 : Action/Func等代理作为参数传递的时候, 因为引用对象的不同会造成或不造成GC, 看下图 :

      Call函数的参数Action, 不使用lambda时, 如果直接使用Func/Func_S函数作为参数, 产生了GC , 使用lambda时, 引用成员变量产生了GC, 引用静态变量, 没有产生GC, 引用成员函数Func产生GC, 引用静态函数Func_S没有产生GC. 这就比价好理解它的本质了:

    1. Lambda如果只引用了静态对象, 那么它就在编译期间就编译好了

    2. Lambda如果引用成员变量, 那么肯定在每次执行的时候重新创建或者重置了上下文

    3. 函数都没办法在编译期间转化成Action/Func, 作为参数每次都会进行类型转换(或者是new成了Action等)产生新对象, 所以在传入参数时, Lambda比函数更有可能节省资源. 与我们对Lambda的印象相反.

      再补充一个, 在引用成员变量的时候, 可以这样规避GC :

        private void Call(int value, System.Action<int> call)
        {
            call.Invoke(value);
        }
        private void Call(System.Action call)
        {
            call.Invoke();
        }
        private int aa = 100;       // 成员变量
        void Update()
        {
            // 0B GC
            Call(aa, (_val) =>
            {
                if(_val > 100)
                {
                }
            });
    
            // 104B
            Call(() =>
            {
                if(aa > 100)
                {
                    
                }
            });
        }

      上文中说的, 直接引用成员变量, 使得每个Lambda的上下文都不一样了, 所以每次都会创建新对象造成GC, 而在这里第一个Call, 它创建的Lambda的上下文就是Call传给它的value, 所以上下文没有改变, 是一个静态编译.

      总结来说就是回调函之类的传入代理作为参数的情况, 都有可能造成GC, 并且大多数都有规避的方案, 唯一难以处理的就是代理中被引用的资源的问题, 在你觉得资源已经没有被引用而调用 UnloadUnusedAssets 的时候无法清除资源, 而即使调用GC也不会立即触发GC回收代理对象而解除资源的引用的时候, 会很郁闷. 

  • 相关阅读:
    Redis 常用模式思路
    MacOS Catalina 10.15 利用shell脚本启动NGINX、MySQL、PHP
    windows上 python有多版本,如何管理,如何区别?
    20180813视频笔记 深度学习基础上篇(1)之必备基础知识点 深度学习基础上篇(2)神经网络模型视频笔记:深度学习基础上篇(3)神经网络案例实战 和 深度学习基础下篇
    20180813视频笔记 深度学习基础上篇(1)之必备基础知识点 深度学习基础上篇(2)神经网络模型
    数据集
    基于深度学习的目标跟踪
    使用GitHub+Hexo建立个人网站,并绑定自己的域名(Ubuntu环境下)
    使用jemdoc制作个人主页
    《2017全球人工智能人才白皮书》发布丨解读世界顶级AI牛人的秘密——腾讯研究院
  • 原文地址:https://www.cnblogs.com/tiancaiwrk/p/11803674.html
Copyright © 2020-2023  润新知