由于闭包引起的内存泄漏

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

  在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回收代理对象而解除资源的引用的时候, 会很郁闷. 

猜你喜欢

转载自www.cnblogs.com/tiancaiwrk/p/11803674.html