在上期的内存专题中,我们和大家分享了项目开发中的内存分配情况。除此之外, 还有三个更为重要的地方需要研发团队关注:内存泄露、Mono无效堆内存开销和资源冗余。这几乎是所有团队在研发过程中都会遇到的问题。今天我们就来详细说一说这些问题的解决方案。
同时,不同的项目遇到的问题不尽相同,欢迎大家加入到UWA的产品QQ群(793972859),就相关内容来进一步讨论。
内存泄露
内存泄露是开发人员在项目研发过程中最常见也最不愿遇到的问题。就目前来看,大家对于判断项目是否存在内存泄露仍然存在一些误区:
误区一
我的项目进出场景前后内存回落不一致,比如进入场景后,内存增加40MB,出来后下降30MB,仍有10MB内存没有返回给系统,即说明内存存在泄露情况。
误区二
我的项目在进出场景前后,Unity Profiler中内存回落正常,但Android的PSS数值并没有完全回落(出场景后的PSS值高于进场景前的PSS值),即说明内存存在泄露情况。
以上是我们遇到的开发团队反馈给我们的典型问题。相信大多数开发团队都会遇到类似的情况。在此有必要说明一下,以上两种情况均不能表明内存存在泄漏问题。即便内存在一段时间始终保持增长的趋势,也不能简单地判定其存在内存泄露。因为造成内存不能完全回落的情况有很多,比如资源加载后常驻内存以备后续使用、Mono堆内存的只升不降等等,这些均可造成内存无法完全回落。一般来说,我们推荐的判断内存是否泄漏的方法如下:
一、检查资源的使用情况,特别是纹理、网格等资源的使用
在我们进行过的项目深度优化过程中,资源泄漏是内存泄露的主要表现形式,其具体原因是用户对加载后的资源进行了储存(比如放到Container中),但在场景切换时并没有将其Remove或Clear,从而无论是引擎本身还是手动调用Resources.UnloadUnusedAssets等相关API均无法对其进行卸载,进而造成了资源泄露。对于这种情况的排查相当困难,这是因为项目中的资源量过于巨大,泄露资源往往很难定位。因此,我们在UWA测评报告中对项目中的每个资源都进行了详细的监控,并通过“生命周期”这一衡量指标让大家可以清楚地了解到每个资源在项目运行过程中的使用范围。
这样,大家可以通过资源的“生命周期”属性来快速查看有哪些资源是“常驻”内存的,并且判断该资源是“预加载”资源还是“泄露”资源。
同时,项目中所使用的总资源数量往往是成百上千的,让大家逐个资源检查过来是一件很费力的事情。所以,我们推出了资源的“场景比较”功能。建议大家通过以下两种方式进行资源比较,以便更快地找到存在“泄露”问题的资源:
同种类型场景或同一场景进行比较
一般来说,同种场景或同一场景的资源使用应该是较为固定的,比如游戏项目中的主城场景或主界面场景。通过比较不同时刻同一场景的资源信息,可以快速帮你找到其资源使用的差异情况。这样,你只需判断这些“差异”资源的存在是否合理,即可快速判定是否存在资源泄露,已经具体的泄露资源。
不同类型场景进行比较
除一些常驻资源外,不同类型的场景,其资源使用是完全不同的。比如,游戏中主城和战斗副本的资源,除少部分常驻内存的资源外,二者使用的绝大部分资源应该是不一致的。所以,通过比较两种不同类型的场景,你可以直接查看比较结果中的“共同资源”,并判断其是否确实为预先设定好的常驻资源。如果不是,则它很可能是“泄露”资源,需要你进一步查看项目的资源管理是否存在漏洞。
二、通过Profiler来检测WebStream或SerializedFile的使用情况
AssetBundle的管理不当也会造成一定的内存泄露,即上一场景中使用的AssetBundle在场景切换时没有被卸载掉,而被带入到了下一场场景中。对于这种情况,建议直接通过Profiler Memory中的Take Sample来对其进行检测,通过直接查看WebStream或SerializedFile中的AssetBundle名称,即可判断是否存在“泄露”情况。
三、通过Android PSS/iOS Instrument反馈的App线程内存来查看
承接上述“误区二”中的说法,“Unity Profiler中内存回落正常,但Android的PSS数值并没有完全回落”是有可能的,这是因为Unity Profiler反馈的是引擎的真实分配的物理内存,而PSS中记录的则包括系统的部分缓存。一般情况下,Android或iOS并不会及时将所有App卸载数据进行清理,为了保证下次使用时的流畅性,OS会将部分数据放入到缓存,待自身内存不足时,OS Kernel会启动类似LowMemoryKiller的机制来查询缓存甚至杀死一些进程来释放内存。因此,并不能通过一两次的PSS内存没有完全回落来说明内存泄露问题。
我们推荐的测试方式是在两个场景之间来回不停切换,比如主城和战斗副本间。理论上来说,多次切换同样的场景,如果Profiler中显示的Unity内存回落正常,那么其PSS/Instrument的内存数值波动范围也是趋于稳定的,但如果出现了PSS/Instrument内存持续增长的情况,则需要大家注意了。这可能有两种可能:
Unity引擎自身的内存泄露问题。这种概率很小,之前仅在少数版本中出现过。
第三方插件在使用时出现了内存泄露。这种概率较大,因为Profiler仅能对Unity自身的内存进行监控,而无法检测到第三方库的内存分配情况。因此,在出现上述内存问题时,建议大家先对自身使用的第三方库进行排查。
无效的Mono堆内存开销
目前,Unity所使用的Mono版本中存在一个较大的问题,即内存一旦分配,则不会再返回给系统。这就衍生出另外一个问题—— 无效的Mono堆内存。它是Mono所分配的堆内存,但却没有被真正利用上,因此称之为“无效”。那么,如何查看我的项目中是否存在较大量的“无效堆内存”呢?
在UWA测评报告中,我们提供了内存随项目运行的分配情况,如下图所示。其中,蓝线和紫线的分离情况,反映了无效堆内存的分配大小。比如,图中所选中时刻,蓝线的Reserved Total为当前项目所占据的总物理内存,而紫线的Used Total为当前项目所使用的总物理内存,这说明当前项目中的空闲内存为57.1MB(200.4-143.3),而这其中主要由两部分组成,空闲的Unity引擎内存和无效的Mono堆内存。其中,空闲的Unity内存为17.1MB(92.0-74.9),所以当前所选帧的无效Mono堆内存为40.0MB。并且,从图中可以看出,蓝线和紫线在运行过程中一直分得较开,这说明一直存在不小的Mono堆内存处于“无效”状态。这是一件很浪费的事情,特别是对于内存寸土寸金的移动设备而言。
那么,我们应该如何避免或减少过多“无效堆内存”的分配呢?我们推荐的做法如下:
避免一次性堆内存的过大分配。Mono的堆内存也是“按需”逐步进行分配的。但如果一次性开辟过大堆内存,比如New一个较大Container、加载一个过大配置文件等,则势必会造成Mono的堆内存直接冲高,所以研发团队对堆内存的分配需要时刻注意;
避免不必要的堆内存开销。UWA测评报告中将项目运行过程中堆内存分配Top10函数进行罗列,限于篇幅,我们不再此处进行一一赘述,研发团队可以直接查看之前一篇的内存优化相关文章。
资源冗余
在内存管理方面,还有一个大家必须关注的话题——资源冗余。在我们测评过的大量项目中,95%以上的项目均存在不同程度的资源冗余情况。所谓“资源冗余”,是指在某一时刻内存中存在两份甚至多份同样的资源。导致这种情况的出现主要有两种原因:
一、AssetBundle打包机制出现问题
同一份资源被打入到多份AssetBundle文件中。举个例子,同一张纹理被不同的NPC所使用,同时每个NPC被制作成独立的AssetBundle文件,那么在没有针对纹理进行依赖打包的前提下,就会出现该张纹理出现在不同的NPC AssetBundle文件中。当这些AssetBundle先后被加载到内存后,内存中即会出现纹理资源冗余的情况。对此,我们建议研发团队在发现资源冗余问题后,对相关AssetBundle的制作流程一定要进行检查。
同时,我们在UWA测评中为每个资源引入了一个衡量指标——“数量峰值”。它指的是同一资源在同一帧中出现的最大数量。如果大于1,则说明该资源很可能存在 “冗余资源”。大家可以通过这一列进行排序,即可立即查看项目中的资源冗余情况。
二、资源的实例化所致
在Unity引擎中,当我们修改了一些特定GameObject的资源属性时,引擎会为该GameObject自动实例化一份资源供其使用,比如Material、Mesh等。以Material为例,我们在研发时经常会有这样的做法:在角色被攻击时,改变其Material中的属性来得到特定的受击效果。这种做法则会导致引擎为特定的GameObject重新实例化一个Material,后缀会加上(instance)字样。其本身没有特别大的问题,但是当有改变Material属性需求的GameObject越来越多时(比如ARPG、MMORPG、MOBA等游戏类型),其内存中的冗余数量则会大量增长。如下图所示,随着游戏的进行,实例化的Material资源会增加到333个。虽然Material的内存占用不大,但是过多的冗余资源却为Resources.UnloadUnusedAssets API的调用效率增加了相当大的压力。
一般情况下,资源属性的改变情况都是固定的,并非随机出现。比如,假设GameObject受到攻击时,其Material属性改变随攻击类型的不同而有三种不同的参数设置。那么,对于这种需求,我们建议你直接制作三种不同的Material,在Runtime情况下通过代码直接替换对应GameObject的Material,而非改变其Material的属性。这样,你会发现,成百上千的instance Material在内存中消失了,取而代之的,则是这三个不同的Material资源。其中的益处,对于能够阅读到这里的你来说,应该已经不需要我多说了。:)
以上则是我们在内存优化工作中的经验和心得,希望它对你的项目研发有所帮助。优化永远没有统一的标准方案,只有最适合你项目的方案,希望大家可以活学活用,不要放过任何一处让你感觉“不对劲”的地方。最后提醒大家——“勿以善小而不为,勿以恶小而为之”,共勉!