四年前,我们一一讲解了Unity主流模块的性能优化知识点,俗称“小白版”。 随着这几年引擎本身、硬件设备、生产标准等的升级,UWA也在不断更新自己的优化规则和方法,源源不断地输出给开发者。 作为性能优化手册的“升级版”,【Unity性能优化系列】将尽量用通俗易懂的表达方式造福于更多的开发者。 本期分享资源管理相关的知识点。
经常有团队问:为什么我的游戏加载这么慢,能不能像***游戏一样秒切场景? 游戏火爆的原因是什么? 为了不让辛苦的游戏卡在真机上或者秒暖手宝,我们必须充分利用UWA报告中的加载模块和资源管理模块,我们将在后面逐步分析未来。
1.加载模块注意事项
这里先给大家普及一些loading相关的attention指标。 下图是Overview报告中加载模块的tab。 我们可以看到左侧有几个重要的参数指标:
1.加载.更新预加载
这是Unity引擎的主要加载功能。 这一项一般在切换场景或者异步加载资源时开销较大。 一般来说,加载的资源越多、越复杂,Load.UpdatePreloading 的耗时就越多。
在优化该功能之前,建议定位其耗时瓶颈。 通过上报的CPU调用栈,可以查看函数在运行过程中详细的堆栈趋势,对函数的耗时分配情况一目了然,从而有针对性地进行优化。
2. 资源。 卸载未使用的资产
这个功能是指卸载不用的资源。 开销主要取决于场景中资产和对象的数量。 数量越多,就越费时。 在优化性能的时候,除了耗时峰值,我们还需要关注这个函数的调用次数。
一般情况下,场景切换过程中引擎会自动调用一次,UWA建议每隔10-15分钟手动调用一次。
同时,研发团队可以尝试在游戏运行时通过Resources.UnloadAsset移除一个已经确定不再使用的资源。 这个API对于移除单个资源是非常高效的,同时也可以减轻统一处理时Resources.UnloadUnusedAssets的压力。
下图是报表中加载相关函数的栈信息。 在堆栈中,GarbageCollectAssetsProfile 是由调用 Resources.UnloadAssetsUnused 引起的。 如果此项占用过多,需要注意是否主动调用Resrouces.UnloadUnusedAssets过于频繁。
3.气相色谱。 收藏
GC调用的频率主要受堆内存的影响。 函数分配堆内存的频率越高,GC 就会来得越快。 因此,当我们的GC.Collect函数被频繁调用时(如下图所示),尤其是随着游戏运行时间的增加,越来越频繁,我们需要注意是否有函数分配高且频繁的堆内存后操作,这部分可以使用GOT Online的Mono模式,检查是否有Mono分配过快或过高的现象。
4.实例化
这里统计的是资源实例化的耗时。 项目的资源越复杂,实例化的次数越多,卡顿的感觉就越明显,但这部分往往容易被大家忽略。 UWA是如何处理好这部分的呢? 问题? 下面我们将结合UWA真机测试报告中的【资源管理】模块进行具体说明。
2.资源管理
这里的资源管理指的是资源调用频率和耗时等策略,因为影响加载体验的无外乎两个角度:加载频率和每次加载耗时。 在真机测试报告中,我们可以在【资源管理】选项卡后看到如下检测项:
功能这么多,我们应该注意哪些细节呢? 说以下核心点:
1.关注加载耗时
无论是AssetBundle还是资源加载,都需要重点关注耗时的。 这里我们打开一个资源加载标签,下面可以看到整个运行过程中的资源调用详情,最后一栏是耗时。
在资源特定信息中,检查资源以查看其在运行期间的调用详细信息。 对应上面的截图,我们可以进一步查看加载AssetBundle是否耗费了这么多时间。
2、集中于短时间内的密集通话
无论是AssetBundle还是资源加载,都要注意加载的频率。 通常,对于频繁加载的对象,我们可以创建一个缓存池,加载一次就添加到缓存中,以后就不需要再加载了。
如下图,这些频繁加载的AssetBundle可能每次有5ms或者50ms的耗时,后面可以直接0。
这里同样需要注意的是,同一个资源在一帧内被多次加载的问题。
如下图,在这一帧调用了5次,是错误的。
3.注意资源缺失
在资源加载列表中,有些项目会出现[不存在]的资源,说明这些资源都是因为不在指定路径下而加载失败的资源。 一般情况下,此类资源是由于版本迭代、删除/迁移、对应代码没有修改/注释造成的。
加载这些[不存在的]资源只会造成少量的CPU开销,但更重要的是,对这些[不存在的]资源进行故障排除可以避免导致崩溃和卡顿的逻辑问题。
4.频繁实例化/销毁
具有大量操作或高时间消耗的资源。 频繁的Instantiate会造成一定量的堆内存分配,从而加快系统调用GC的频率。 更重要的是,频繁的实例化会造成一定的CPU耗时峰值,从而影响游戏的流畅度,所以这部分也是我们需要注意的。
对于这种频繁实例化的资源,使用缓存池来复用实例化次数过多的GameObject,从而减少GameObject实例化的耗时。
5.激活和停用
这种排查方法和实例化类似,重点看调用频率和耗时。
比较Activate和Deactivate的调用次数,因为如果两者相差太大,说明存在无用的Activate/Deactivate操作。
比如某个资源的Activate操作数很高(下图中的Gold_2和Gold_4),为什么Activate操作数这么高? 有必要吗? 我们可以复制资源名称,在Deactivate资源列表中搜索,看看是否真的需要那么多状态激活。
停用 Gold_2
为 Gold_4 停用
这说明10000多次Deactivate操作的差异是没有意义的。
对于以上资源,我们可以在C#端创建一个特例缓存来记录这个对象的Active状态(True或False)。 在调用SetActive之前,先判断当前状态是否已经是我们要切换到的状态,如果没有调用。 这是因为SetActive的操作会从C#转到C++层unity加载场景进度条,所以我们可以通过在C#中进行状态判断来减少这种跨语言的操作,从而避免不必要的耗时。
6.AssetBunde常驻优化
之所以关注这个参数,是因为它会影响项目运行过程中的内存占用。 要知道有一部分Unity内存和AssetBundle驻留导致的Serializedfile有关。 一般来说,我们建议控制的AssetBundle资源数量在1000个以下。考虑到这个指标与项目本身的复杂度有关贴图笔刷,需要自己做一些实验来平衡CPU和内存的平衡。
使用缓冲池可以优化资源的加载,AssetBundle的加载类似。 频繁加载同一个AssetBundle通常是不合理的(如下图所示)。 对于频繁加载和卸载的AssetBundle,建议将其添加到缓存中并留在内存中。
3.Shader.Parse/CreateGPU程序
如果没有正确解析和加载Shader资源游戏图片素材,也会造成较大的CPU开销。 由于Shader的内存占用小,但是加载时间比较长,我们建议最好在项目开始运行的时候就加载所有的Shader资源,然后缓存起来。
1.着色器。 解析
这个函数的耗时主要是Shader的加载和解析,通常是Shader的反复加载。 优化的时候要看具体的Shader加载情况。 具体可以从以下三点入手:
(1)避免使用Standard,使用其他Shader代替Standard Shader。 注意检查Standard Shader是否因模型导入而加载到AssetBundle中;
(2)解决Shader冗余问题,这部分可以结合Shader内存趋势来看,如下图。
如果你的Shader资源没有缓存在内存中,当你切换出场景时Shader会被释放,切入场景时会加载Shader,造成大量的重复开销。 解决这个问题,只需要将Shader剥离,通过dependencies做成一个单独的AB,然后加载完缓存不用卸载unity加载场景进度条,后面就不需要加载Shader了。
(3)减少Shader的Keyword。
研发团队可参考以下资料:
《一种Shader变体收集和打包编译优化思路》
/问题/5da86670e84db43d6efbda72
2.Shader.CreateGPU程序
该API的CPU使用率是Shader第一次渲染时创建GPU程序的耗时,耗时与渲染Shader的复杂度有关。 对此,建议研发团队通过ShaderVariantCollection加载Shader,加载后warmup,避免游戏运行时Shader生成耗时的Shader.CreateGPUProgram。
以上是加载优化时需要注意的一些问题。 如何操作需要你结合项目的实际情况。 同时,结合UWA的在线测评服务,可以快速帮您定位性能瓶颈。