项目一运行,占用的内存越来越多,不会释放,导致GC越来越频繁,越来越慢,这些都是为什么呢,今天从UI方面谈起。
首先让我们来聊聊什么是内存泄漏呢?
一般来讲内存泄漏就是指我们的应用向内存申请了一块地址,然后这块地址的相关引用全部丢失了,这块内存无法再被分配,在计算机眼里,那就是丢了,找不回来了,除非重启。。。
不过,这里如果我们要去理解Unity中的内存泄漏,那我们首先要了解一下Unity的内存分配机制和GC机制,哇,不过说真的,要真是细说这两点,那真是几天都讲不完呀,还是算了,哈哈,这里大概聊一下,
程序在运行的时候,会先从计算机中申请一块内存,这时候如果我们需要去申请一块地址的时候,Unity会先去从堆内存中找合适大小的地址块给我们,但是这时候,如果堆内存用完了,这时候GC就出马了,会先清理一遍当前内存中无用的数据然后给我们分配所需要的内存块,那这个时候如果GC之后还是没有找到足够大小的内存给我们用怎么办呢,Unity只能去在申请一块之前内存2倍大小的内存了。
这时候来想想,如果在我们的项目中这如果不断重复上述步骤,那么这时候是不是就意味着内存泄漏了呢。。。 现在就让我们开始从实际情况来一探究竟吧!!!
一开始我们通过Unity的Profiler工具只能看到在我们的UI已经关闭销毁了可是UI里面用到的图集还在内存里面存在,不应该呀,如果图集不释放,那岂不是意味着我们如果打开很多UI的时候,这些图集资源就要占到很多内存,如何查看当前内存中图集情况,可以参考下图,先选中Memory模块,然后选择Detailed,点击Take Sample Playmode,这时候内存中的图集就出现在下面了,参考5的位置,这里说明一下位置4这个选项,如果不勾选,进行内存采样速度会快很多,勾选了会慢很多,但是会同时采样出对应资源当前的引用情况。
这时候我们通过对游戏中不同节点进行内存采样,便能分析出我们哪些图集没有随着预设的销毁而销毁。
问题已经找到了,那么如何解决呢,如何下手呢,这时候又不知道怎么办了,害!!!
但是生活还要继续,问题还得解决呀,那么接下来就开始了问题分析,无数次Demo测试,从AB包加载卸载,到Unity内存分配管理,从GC的工作方式,到GC的底层实现原理,终于发现了这几个问题。
首先,如果我们的项目是通过AssetBundle方式加载的,那么在我们切场景或者进行阶段变化的时候我们需要处理一下无用资源的释放,调用一下下面的接口。
Resources.UnloadUnusedAssets();
卸载未使用的资源
这时候我们在进行内存对比分析的时候会发现会有一些内存被释放,可是图集不销毁的问题还在,害,还以为挺简单的,目前看来问题更复杂了。。。
这时候用上了另一个工具Memory Profiler,这个工具是在Unity2020之后的版本推出的功能,对当前内存进行快照,可视化的形式显示当前内存分配的大小,列出了每个托管对象的类型,值,占用大小,地址,被引用链等等信息,还可以进行快照对比,分析两次内存快照新增、删除和保持不变的内存对象,从而更方便快捷的定位项目内存的使用情况。
通过对内存进行快照,分析图集的引用链,屏蔽代码,重新快照测试,一次次的测试,慢慢缩小代码范围,定位图集不销毁的原因,最终发现原来是我们的UI使用了static实例来实现单例效果,在其他地方调用,但是在我们UI不需要的时候并没有将这个静态单例设置为null,导致整个UI资源的相关引用一直存在,无法释放,还有就是我们在对按钮进行事件注册的时候,使用了项目封装的接口,而项目封装的接口在拿到委托事件对象后,并没有在移除事件的时候去清除委托事件对象,导致引用一直存在,相关的资源也就无法释放。
相信经过上述步骤之后我们的图集不销毁问题已经解决了大部分了,具体还有哪些,后面有需要我们再补充,哈哈。
这里再说一个图片不销毁问题,在项目中我们经常会去动态替换某些图片来实现我们的功能,这时候有一个统一接口就很方便了,可是图片不销毁问题也正好跟这个动态替换接口有关,由于我们的统一接口会保存一份加载的图片的引用,在对应预设销毁的时候,由于图片引用一直存在,所以图片就无法被GC处理掉,这时候我们可以考虑对我们动态加载的图片进行场景管理,在合适的时候清空一次引用列表,还有由于我们动态图片加载是自己管理加载资源,所以我们在清空列表的时候要调用一次对应接口的卸载资源接口,否则,资源还是无法从内存中释放。
目前为止,图集图片不销毁问题已经解决了大部分,至于项目中具体还有没有其他问题导致,有待后续研究,,,总结一下:
- 使用了static静态类方式来实现单例的UI,在使用完之后一定记得将对应单例设置为null,让GC可以去释放对应的内存。
- 在使用委托或者其他时候,拿到类对象的引用在使用完之后一定要记得释放引用。
- 加载的资源在不适用的时候记得卸载掉,比如AssetBundle.Load()和AssetBundle.Unload()
- 在适当的时机调用Resource.UnloadUnusedAssets()接口释放无用的资源
简而言之,言而简之,内存优化一直是项目开发中的重头戏,任重而道远呀。。。
心怀梦想 奔向远方