6

Unity性能优化系列—加载与资源管理

 3 years ago
source link: https://blog.uwa4d.com/archives/UWA_ReportModule2.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

我们曾在四年前对于Unity的主流模块的性能优化知识点逐一做过讲解,俗称“小白版”。随着这几年引擎本身、硬件设备、制作标准等等的升级,UWA也不断更新优化规则和方法并持续输出给广大开发者。作为"升级版"的性能优化手册,【Unity性能优化系列】将力图以浅显易懂的表达,让更多开发者受用。本期就将分享加载与资源管理相关的知识点。

经常会有一些团队询问:为什么我的游戏加载这么缓慢呢,能否做到像***游戏一样秒切场景呢?游戏发烫又是什么原因呢?为了防止大家辛苦做出来的游戏在真机上卡成翔或者秒变暖手宝,我们要充分利用好UWA报告中的加载模块和资源管理模块,下文我们将来逐步分析。

一、加载模块中的关注项

这里我们先来科普一些加载相关的关注指标。下图是Overview报告中的加载模块的页签,我们看到左边有这么几项重要参数指标:

1.png

1、Loading.UpdatePreloading
这是Unity引擎最主要的加载函数。该项一般在切换场景时或异步加载资源时开销较大。一般来说,加载资源越多越复杂,则其Load.UpdatePreloading的耗时也越大。

2.png

该函数优化前,建议先定位其耗时占用瓶颈。通过报告的CPU调用堆栈即可查看该函数在运行过程中的详细堆栈走势,对函数的耗时分配一目了然,从而有的放矢的进行优化。

3.png

2、Resources.UnloadUnusedAssets
该函数指卸载未使用的资源,开销主要取决于场景中的Assets和Object的数量,数量越多,则耗时越高。在性能优化时,除了耗时峰值之外,我们还需关注该函数的调用次数。

4.png

一般情况下,场景切换过程中, 引擎会自动调用一次,UWA建议在10~15分钟的时候手动调用一次。

同时,研发团队可尝试在游戏运行时,通过Resources.UnloadAsset来去除已经确定不再使用的某一资源,该API对于去除单一资源的效率很高,同时也可以降低Resources.UnloadUnusedAssets统一处理时的压力。

下图为报告中加载相关函数的堆栈信息,在堆栈中GarbageCollectAssetsProfile是由于调用了Resources.UnloadAssetsUnused导致的,如果此项占用过高,则需要关注是否主动调用Resrouces.UnloadUnusedAssets过于频繁。

5.png

3、GC.Collect
GC调用频率主要受堆内存影响,当函数的堆内存分配量越多、越频繁,GC就会越快到来。所以当我们的GC.Collect函数的调用频率较为频繁(如下图所示),特别是随着游戏运行时间增加,越来越频繁时,就需要我们留意是否存在高分配、频繁分配堆内存的函数操作了,这部分就可以借助GOT Online的Mono模式排查是否有Mono分配过快或过高的现象。

6.png

4、Instantiate

7.png

这里统计的是资源实例化的耗时,当项目的资源越复杂、实例化数量越多,卡顿感就越明显,但这部分往往是被大家容易忽略的,那UWA是如何处理好这部分的问题呢?下文我们将结合UWA真人真机测试报告中【资源管理】模块来进行具体讲解。


二、资源管理

这里的资源管理讲的是资源的调用频率、耗时等策略,因为影响加载体验的无非两个角度:加载的频率和每加载一次的耗时。在真人真机测试的报告中,我们可以看到【资源管理】标签后,包含以下检测项:

8.png

这么多功能,我们要关注哪些细节呢?说下几个核心点:

1、关注耗时较高的加载
无论是AssetBundle还是资源加载,耗时较高的都需要重点关注。这里我们打开一个资源加载的页签,可以看到下方是整个运行过程中的资源调用详情,最后一栏是耗时。

9.png

在资源具体信息中,勾选某个资源,就可以看到它在运行过程中的调用细节。对应上面的截图,我们可以进一步排查下这个AssetBundle加载是否需要那么多耗时。

10.png

2、短期时间内调用次数密集的重点关注
无论是AssetBundle还是资源加载,都要关注加载的频率。通常对于频繁加载的对象,我们可以通过建立缓存池的方法,先加载一次后将其加入缓存,后续就无须进行加载了。

如下图中,这些频繁加载的AssetBundle,可能原来有每次5ms或者50ms的耗时,后面可直接为0

11.png

这里再提下,我们也需要留意一帧内相同资源被多次加载的问题。
如下图,这一帧里调用5次,这个是不对的。

12.png

3、留意不存在资源

13.png

在资源加载的列表中,有的项目会出现【不存在】资源的情况,说明这些资源都是由于不在指定路径下导致加载失败的资源。一般情况下,这类资源是伴随着版本迭代,进行删除/迁移后,没有修改/注释对应的代码导致的。

加载这些【不存在】资源仅导致了一小部分CPU开销,但更重要是,排查这些【不存在】可以避免逻辑上的问题导致闪退和卡死等现象。

4、频繁实例化/Destroy
操作次数较高或耗时较高的资源。频繁的Instantiate会造成一定的堆内存分配,从而会加快系统调用GC的频率。更重要的是,频繁的实例化会造成CPU耗时产生一定的峰值,导致游戏的流畅性受到影响,所以这部分也是我们需要关注的。

14.png

对于这种频繁实例化的资源,通过缓存池复用实例化次数过多的GameObject,进而减少GameObject实例化的耗时。

5、Activate和Deactivate
这个排查方式与实例化是类似的,主要关注调用频率和耗时。

15.png

对比Activate和Deactivate的调用次数,因为如两者相差过大,说明存在无用的Activate/Deactivate操作。

16.png

例如,某个资源的Activate操作次数非常多(如下图中的Gold_2和Gold_4),为什么次数那么高?是否有必要呢?我们可复制该资源名称,在Deactivate资源列表中进行搜索查看是否确实需要这么多次状态的激活。

17.png

Gold_2的Deactivate

18.png

Gold_4的Deactivate

19.png

这说明相差的1万多次的Deactivate操作都是无意义的。

对于以上这种资源,我们可以通过在C#端创建一个特例缓存,记录这个对象的Active的状态(True or False),在调用SetActive之前,先判断一下当前的状态是否已经是想要切换到的状态,如果不是才调用。这是因为SetActive的操作是会从C#走到C++层的,所以我们在C#进行状态判断可以减少这种跨语言的操作,从而避免不必要的耗时。

6、AssetBunde驻留优化
之所以关注这个参数,是因为它影响了项目运行过程中的内存占用,要知道Unity内存一部分是由AssetBundle驻留导致的Serializedfile相关的,一般来说我们建议控制的AssetBundle资源数量在1000以下。考虑到这个指标和项目本身的复杂度有关,所以大家需要自身做些实验,好权衡CPU和内存之间的天平。

20.png

资源的加载可以使用缓存池的方式来进行优化,AssetBundle的加载也是类似的。对于同一个AssetBundle进行频繁的加载通常是不合理的(如下图所示),对于频繁加载卸载的AssetBundle,建议将其加入缓存,常驻于内存中。

21.png


三、Shader.Parse/CreateGPUProgram

Shader资源如果解析加载策略不当,也会造成CPU开销较大。由于Shader的内存占用很小,但是加载的耗时又比较高,所以我们建议在理想情况下是在项目开始运行时就把所有的Shader资源全部加载完成,然后缓存。

1、Shader.Parse

22.png

该函数的耗时主要是由于Shader的加载和解析,通常是由于Shader的重复加载导致的,在优化时要看一下具体的Shader加载情况,具体可以从以下三点着手:

(1)避免使用Standard,使用其他Shader代替Standard Shader。注意排查是否因为模型导入而导致Standard Shader被加载进入AssetBundle中;

23.png

(2)解决Shader冗余问题,这部分可以结合Shader的内存走势查看,如下图所示。

24.png

如果大家的Shader资源并不是缓存在内存中的,切出场景时则会释放Shader,切入场景会加载Shader,导致了大量的重复开销。解决这个问题,只需要把Shader进行剥离,通过依赖关系将其做成单独的AB,然后加载后就缓存住不卸载,那么后续就不需要再对此Shader进行加载了。

(3)减少Shader的Keyword。
研发团队可以参考下面的资料:
《一种Shader变体收集和打包编译优化的思路》
https://answer.uwa4d.com/question/5da86670e84db43d6efbda72

2、Shader.CreateGPUProgram

25.png

该API的CPU占用是Shader第一次渲染时创建GPU程序的耗时,其耗时与渲染Shader的复杂程度相关。对此,建议研发团队将Shader通过ShaderVariantCollection进行加载,并在加载后并Warmup,从而避免Shader在游戏运行时产生Shader.CreateGPUProgram的耗时。

以上就是加载在优化时需要关注的一些问题,如何操作还需要大家结合项目实际情况,同时结合UWA的线上测评服务可以快速地帮助大家定位到性能瓶颈。

7.png


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK