分享

由于闭包引起的内存泄漏

 tiancaiwrk 2019-04-02

        在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亦或是函数, 即使在调用之后清空了, 结果都是一样的, 都需要在一次GC之后才会回收这个对象然后才能解除对Texture2D这个对象的引用, 在这里可以把回调函数看成一个类对象, 它里面引用了你的Texture2D, 当你把这个回调置空之后, 它的引用仍然在, 只能在下次GC之后才能被解除. 当引用还在的时候你再次加载资源, 那么资源就在内存中重复了!


补充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, 作为参数每次都会进行类型转换产生新对象

    所以, 在传入参数时, Lambda比函数更有可能节省资源. 与我们对Lambda的印象相反.

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多