我们曾在四年前对于Unity的主流模块的性能优化知识点逐一做过讲解,俗称“小白版”。随着这几年引擎本身、硬件设备、制作标准等等的升级,UWA也不断更新优化规则和方法并持续输出给广大开发者。作为"升级版"的性能优化手册,【Unity性能优化系列】将力图以浅显易懂的表达,让更多开发者受用。本期就将分享加载与资源管理相关的知识点。
经常会有一些团队询问:为什么我的游戏加载这么缓慢呢,能否做到像***游戏一样秒切场景呢?游戏发烫又是什么原因呢?为了防止大家辛苦做出来的游戏在真机上卡成翔或者秒变暖手宝,我们要充分利用好UWA报告中的加载模块和资源管理模块,下文我们将来逐步分析。
这里我们先来科普一些加载相关的关注指标。下图是Overview报告中的加载模块的页签,我们看到左边有这么几项重要参数指标:
1、Loading.UpdatePreloading
这是Unity引擎最主要的加载函数。该项一般在切换场景时或异步加载资源时开销较大。一般来说,加载资源越多越复杂,则其Load.UpdatePreloading的耗时也越大。
该函数优化前,建议先定位其耗时占用瓶颈。通过报告的CPU调用堆栈即可查看该函数在运行过程中的详细堆栈走势,对函数的耗时分配一目了然,从而有的放矢的进行优化。
2、Resources.UnloadUnusedAssets
该函数指卸载未使用的资源,开销主要取决于场景中的Assets和Object的数量,数量越多,则耗时越高。在性能优化时,除了耗时峰值之外,我们还需关注该函数的调用次数。
一般情况下,场景切换过程中, 引擎会自动调用一次,UWA建议在10~15分钟的时候手动调用一次。
同时,研发团队可尝试在游戏运行时,通过Resources.UnloadAsset来去除已经确定不再使用的某一资源,该API对于去除单一资源的效率很高,同时也可以降低Resources.UnloadUnusedAssets统一处理时的压力。
下图为报告中加载相关函数的堆栈信息,在堆栈中GarbageCollectAssetsProfile是由于调用了Resources.UnloadAssetsUnused导致的,如果此项占用过高,则需要关注是否主动调用Resrouces.UnloadUnusedAssets过于频繁。
3、GC.Collect
GC调用频率主要受堆内存影响,当函数的堆内存分配量越多、越频繁,GC就会越快到来。所以当我们的GC.Collect函数的调用频率较为频繁(如下图所示),特别是随着游戏运行时间增加,越来越频繁时,就需要我们留意是否存在高分配、频繁分配堆内存的函数操作了,这部分就可以借助GOT Online的Mono模式排查是否有Mono分配过快或过高的现象。
4、Instantiate
这里统计的是资源实例化的耗时,当项目的资源越复杂、实例化数量越多,卡顿感就越明显,但这部分往往是被大家容易忽略的,那UWA是如何处理好这部分的问题呢?下文我们将结合UWA真人真机测试报告中【资源管理】模块来进行具体讲解。
这里的资源管理讲的是资源的调用频率、耗时等策略,因为影响加载体验的无非两个角度:加载的频率和每加载一次的耗时。在真人真机测试的报告中,我们可以看到【资源管理】标签后,包含以下检测项:
这么多功能,我们要关注哪些细节呢?说下几个核心点:
1、关注耗时较高的加载
无论是AssetBundle还是资源加载,耗时较高的都需要重点关注。这里我们打开一个资源加载的页签,可以看到下方是整个运行过程中的资源调用详情,最后一栏是耗时。
在资源具体信息中,勾选某个资源,就可以看到它在运行过程中的调用细节。对应上面的截图,我们可以进一步排查下这个AssetBundle加载是否需要那么多耗时。
2、短期时间内调用次数密集的重点关注
无论是AssetBundle还是资源加载,都要关注加载的频率。通常对于频繁加载的对象,我们可以通过建立缓存池的方法,先加载一次后将其加入缓存,后续就无须进行加载了。
如下图中,这些频繁加载的AssetBundle,可能原来有每次5ms或者50ms的耗时,后面可直接为0。
这里再提下,我们也需要留意一帧内相同资源被多次加载的问题。
如下图,这一帧里调用5次,这个是不对的。
3、留意不存在资源
在资源加载的列表中,有的项目会出现【不存在】资源的情况,说明这些资源都是由于不在指定路径下导致加载失败的资源。一般情况下,这类资源是伴随着版本迭代,进行删除/迁移后,没有修改/注释对应的代码导致的。
加载这些【不存在】资源仅导致了一小部分CPU开销,但更重要是,排查这些【不存在】可以避免逻辑上的问题导致闪退和卡死等现象。
4、频繁实例化/Destroy
操作次数较高或耗时较高的资源。频繁的Instantiate会造成一定的堆内存分配,从而会加快系统调用GC的频率。更重要的是,频繁的实例化会造成CPU耗时产生一定的峰值,导致游戏的流畅性受到影响,所以这部分也是我们需要关注的。
对于这种频繁实例化的资源,通过缓存池复用实例化次数过多的GameObject,进而减少GameObject实例化的耗时。
5、Activate和Deactivate
这个排查方式与实例化是类似的,主要关注调用频率和耗时。
对比Activate和Deactivate的调用次数,因为如两者相差过大,说明存在无用的Activate/Deactivate操作。
例如,某个资源的Activate操作次数非常多(如下图中的Gold_2和Gold_4),为什么次数那么高?是否有必要呢?我们可复制该资源名称,在Deactivate资源列表中进行搜索查看是否确实需要这么多次状态的激活。
Gold_2的Deactivate
Gold_4的Deactivate
这说明相差的1万多次的Deactivate操作都是无意义的。
对于以上这种资源,我们可以通过在C#端创建一个特例缓存,记录这个对象的Active的状态(True or False),在调用SetActive之前,先判断一下当前的状态是否已经是想要切换到的状态,如果不是才调用。这是因为SetActive的操作是会从C#走到C++层的,所以我们在C#进行状态判断可以减少这种跨语言的操作,从而避免不必要的耗时。
6、AssetBunde驻留优化
之所以关注这个参数,是因为它影响了项目运行过程中的内存占用,要知道Unity内存一部分是由AssetBundle驻留导致的Serializedfile相关的,一般来说我们建议控制的AssetBundle资源数量在1000以下。考虑到这个指标和项目本身的复杂度有关,所以大家需要自身做些实验,好权衡CPU和内存之间的天平。
资源的加载可以使用缓存池的方式来进行优化,AssetBundle的加载也是类似的。对于同一个AssetBundle进行频繁的加载通常是不合理的(如下图所示),对于频繁加载卸载的AssetBundle,建议将其加入缓存,常驻于内存中。
Shader资源如果解析加载策略不当,也会造成CPU开销较大。由于Shader的内存占用很小,但是加载的耗时又比较高,所以我们建议在理想情况下是在项目开始运行时就把所有的Shader资源全部加载完成,然后缓存。
1、Shader.Parse
该函数的耗时主要是由于Shader的加载和解析,通常是由于Shader的重复加载导致的,在优化时要看一下具体的Shader加载情况,具体可以从以下三点着手:
(1)避免使用Standard,使用其他Shader代替Standard Shader。注意排查是否因为模型导入而导致Standard Shader被加载进入AssetBundle中;
(2)解决Shader冗余问题,这部分可以结合Shader的内存走势查看,如下图所示。
如果大家的Shader资源并不是缓存在内存中的,切出场景时则会释放Shader,切入场景会加载Shader,导致了大量的重复开销。解决这个问题,只需要把Shader进行剥离,通过依赖关系将其做成单独的AB,然后加载后就缓存住不卸载,那么后续就不需要再对此Shader进行加载了。
(3)减少Shader的Keyword。
研发团队可以参考下面的资料:
《一种Shader变体收集和打包编译优化的思路》
https://answer.uwa4d.com/question/5da86670e84db43d6efbda72
2、Shader.CreateGPUProgram
该API的CPU占用是Shader第一次渲染时创建GPU程序的耗时,其耗时与渲染Shader的复杂程度相关。对此,建议研发团队将Shader通过ShaderVariantCollection进行加载,并在加载后并Warmup,从而避免Shader在游戏运行时产生Shader.CreateGPUProgram的耗时。
以上就是加载在优化时需要关注的一些问题,如何操作还需要大家结合项目实际情况,同时结合UWA的线上测评服务可以快速地帮助大家定位到性能瓶颈。