在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. 这就比价好理解它的本质了:
|
|
来自: tiancaiwrk > 《基础》