游戏发烫性能优化系列之二:加载模块和资源管理模块

游戏发烫性能优化系列之二:加载模块和资源管理模块

四年前,我们一一讲解了Unity主流模块的性能优化知识点,俗称“小白版”。 随着这几年引擎本身、硬件设备、生产标准等的升级,UWA也在不断更新自己的优化规则和方法,源源不断地输出给开发者。 作为性能优化手册的“升级版”,【Unity性能优化系列】将尽量用通俗易懂的表达方式造福于更多的开发者。 本期分享资源管理相关的知识点。

经常有团队问:为什么我的游戏加载这么慢,能不能像***游戏一样秒切场景? 游戏火爆的原因是什么? 为了不让辛苦的游戏卡在真机上或者秒暖手宝,我们必须充分利用UWA报告中的加载模块和资源管理模块,我们将在后面逐步分析未来。

1.加载模块注意事项

这里先给大家普及一些loading相关的attention指标。 下图是Overview报告中加载模块的tab。 我们可以看到左侧有几个重要的参数指标:

unity 加载场景_unity重新加载场景_unity加载场景进度条

1.加载.更新预加载

这是Unity引擎的主要加载功能。 这一项一般在切换场景或者异步加载资源时开销较大。 一般来说,加载的资源越多、越复杂,Load.UpdatePreloading 的耗时就越多。

unity加载场景进度条_unity 加载场景_unity重新加载场景

在优化该功能之前,建议定位其耗时瓶颈。 通过上报的CPU调用栈,可以查看函数在运行过程中详细的堆栈趋势,对函数的耗时分配情况一目了然,从而有针对性地进行优化。

unity加载场景进度条_unity 加载场景_unity重新加载场景

2. 资源。 卸载未使用的资产

这个功能是指卸载不用的资源。 开销主要取决于场景中资产和对象的数量。 数量越多,就越费时。 在优化性能的时候,除了耗时峰值,我们还需要关注这个函数的调用次数。

unity加载场景进度条_unity重新加载场景_unity 加载场景

一般情况下,场景切换过程中引擎会自动调用一次,UWA建议每隔10-15分钟手动调用一次。

同时,研发团队可以尝试在游戏运行时通过Resources.UnloadAsset移除一个已经确定不再使用的资源。 这个API对于移除单个资源是非常高效的,同时也可以减轻统一处理时Resources.UnloadUnusedAssets的压力。

下图是报表中加载相关函数的栈信息。 在堆栈中,GarbageCollectAssetsProfile 是由调用 Resources.UnloadAssetsUnused 引起的。 如果此项占用过多,需要注意是否主动调用Resrouces.UnloadUnusedAssets过于频繁。

unity 加载场景_unity重新加载场景_unity加载场景进度条

3.气相色谱。 收藏

GC调用的频率主要受堆内存的影响。 函数分配堆内存的频率越高,GC 就会来得越快。 因此,当我们的GC.Collect函数被频繁调用时(如下图所示),尤其是随着游戏运行时间的增加,越来越频繁,我们需要注意是否有函数分配高且频繁的堆内存后操作,这部分可以使用GOT Online的Mono模式,检查是否有Mono分配过快或过高的现象。

unity 加载场景_unity加载场景进度条_unity重新加载场景

4.实例化

unity加载场景进度条_unity 加载场景_unity重新加载场景

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

2.资源管理

这里的资源管理指的是资源调用频率和耗时等策略,因为影响加载体验的无外乎两个角度:加载频率和每次加载耗时。 在真机测试报告中,我们可以在【资源管理】选项卡后看到如下检测项:

unity 加载场景_unity重新加载场景_unity加载场景进度条

功能这么多,我们应该注意哪些细节呢? 说以下核心点:

1.关注加载耗时

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

unity 加载场景_unity重新加载场景_unity加载场景进度条

在资源特定信息中,检查资源以查看其在运行期间的调用详细信息。 对应上面的截图,我们可以进一步查看加载AssetBundle是否耗费了这么多时间。

unity 加载场景_unity重新加载场景_unity加载场景进度条

2、集中于短时间内的密集通话

无论是AssetBundle还是资源加载,都要注意加载的频率。 通常,对于频繁加载的对象,我们可以创建一个缓存池,加载一次就添加到缓存中,以后就不需要再加载了。

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

unity加载场景进度条_unity重新加载场景_unity 加载场景

这里同样需要注意的是,同一个资源在一帧内被多次加载的问题。

如下图,在这一帧调用了5次,是错误的。

unity重新加载场景_unity加载场景进度条_unity 加载场景

3.注意资源缺失

unity加载场景进度条_unity重新加载场景_unity 加载场景

在资源加载列表中,有些项目会出现[不存在]的资源,说明这些资源都是因为不在指定路径下而加载失败的资源。 一般情况下,此类资源是由于版本迭代、删除/迁移、对应代码没有修改/注释造成的。

加载这些[不存在的]资源只会造成少量的CPU开销,但更重要的是,对这些[不存在的]资源进行故障排除可以避免导致崩溃和卡顿的逻辑问题。

4.频繁实例化/销毁

具有大量操作或高时间消耗的资源。 频繁的Instantiate会造成一定量的堆内存分配,从而加快系统调用GC的频率。 更重要的是,频繁的实例化会造成一定的CPU耗时峰值,从而影响游戏的流畅度,所以这部分也是我们需要注意的。

unity 加载场景_unity重新加载场景_unity加载场景进度条

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

5.激活和停用

这种排查方法和实例化类似,重点看调用频率和耗时。

unity加载场景进度条_unity重新加载场景_unity 加载场景

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

unity加载场景进度条_unity 加载场景_unity重新加载场景

比如某个资源的Activate操作数很高(下图中的Gold_2和Gold_4),为什么Activate操作数这么高? 有必要吗? 我们可以复制资源名称,在Deactivate资源列表中搜索,看看是否真的需要那么多状态激活。

unity 加载场景_unity加载场景进度条_unity重新加载场景

停用 Gold_2

unity加载场景进度条_unity 加载场景_unity重新加载场景

为 Gold_4 停用

unity加载场景进度条_unity重新加载场景_unity 加载场景

这说明10000多次Deactivate操作的差异是没有意义的。

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

6.AssetBunde常驻优化

之所以关注这个参数,是因为它会影响项目运行过程中的内存占用。 要知道有一部分Unity内存和AssetBundle驻留导致的Serializedfile有关。 一般来说,我们建议控制的AssetBundle资源数量在1000个以下。考虑到这个指标与项目本身的复杂度有关贴图笔刷,需要自己做一些实验来平衡CPU和内存的平衡。

unity加载场景进度条_unity重新加载场景_unity 加载场景

使用缓冲池可以优化资源的加载,AssetBundle的加载类似。 频繁加载同一个AssetBundle通常是不合理的(如下图所示)。 对于频繁加载和卸载的AssetBundle,建议将其添加到缓存中并留在内存中。

unity 加载场景_unity重新加载场景_unity加载场景进度条

3.Shader.Parse/CreateGPU程序

如果没有正确解析和加载Shader资源游戏图片素材,也会造成较大的CPU开销。 由于Shader的内存占用小,但是加载时间比较长,我们建议最好在项目开始运行的时候就加载所有的Shader资源,然后缓存起来。

1.着色器。 解析

unity加载场景进度条_unity重新加载场景_unity 加载场景

这个函数的耗时主要是Shader的加载和解析,通常是Shader的反复加载。 优化的时候要看具体的Shader加载情况。 具体可以从以下三点入手:

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

unity加载场景进度条_unity 加载场景_unity重新加载场景

(2)解决Shader冗余问题,这部分可以结合Shader内存趋势来看,如下图。

unity重新加载场景_unity加载场景进度条_unity 加载场景

如果你的Shader资源没有缓存在内存中,当你切换出场景时Shader会被释放,切入场景时会加载Shader,造成大量的重复开销。 解决这个问题,只需要将Shader剥离,通过dependencies做成一个单独的AB,然后加载完缓存不用卸载unity加载场景进度条,后面就不需要加载Shader了。

(3)减少Shader的Keyword。

研发团队可参考以下资料:

《一种Shader变体收集和打包编译优化思路》

/问题/5da86670e84db43d6efbda72

2.Shader.CreateGPU程序

unity 加载场景_unity加载场景进度条_unity重新加载场景

该API的CPU使用率是Shader第一次渲染时创建GPU程序的耗时,耗时与渲染Shader的复杂度有关。 对此,建议研发团队通过ShaderVariantCollection加载Shader,加载后warmup,避免游戏运行时Shader生成耗时的Shader.CreateGPUProgram。

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

unity加载场景进度条_unity重新加载场景_unity 加载场景