最近主要在做UI的性能优化,在此记录一下。
UI优化无非几个点:UI重建、DrawCall、Overdraw、其它一些细碎的地方,我在优化的时候也是按照这个顺序,因为UI重建的危害要大于DrawCall,且两者的优化思路会有冲突的地方,尽量以重建的优化为优先。
UI重建
通过Profiler发现在战斗中会出现很多UI模块的耗时峰值,如图所示: 主要涉及到两个函数: UGUI.Rendering.UpdateBatches:在主线程中执行重建 PostLateUpdate.PlayerEmitCanvasGeometry:等待Job中的重建函数执行 通过阅读源码发现,UI重建由引发原因的不同可以分为两种情况: ReSort:Layout层级结构变化引起 ReBatch:UI元素的属性(位置、旋转、缩放、颜色等)发生变化,以及ReSort
其中,ReSort会导致父Canvas的重建,ReBatch某些情况下会导致父Canvas的重建。ReSort的耗时主要受Canvas层级结构的影响,包括父Canvas以及父Canvas下所有子Canvas的结构。ReBatch的耗时主要受此Canvas中显示的UI元素数量影响(Alpha不为0,Scale不为0,Active为true,有CanvasRenderer组件),除此之外,同一帧内改变UI元素的数量,短时间内改变UI元素的频率,会导致耗时增加。 根据上述特性,对现在的InGameUI进行了如下修改:
修改项 | 原因 |
---|---|
把Canvas上的Canvas组件移除,且Canvas不加入XUIRoot下 | 解决子Canvas下节点变化导致的父节点ReSort |
游戏内需要切换显隐状态的UI,避免使用SetActive,尤其是Normal层的UI | |
有CanvasGroup用CanvasGroup,没有就设置缩放 | 节点开启或关闭会引起ReSort |
检查Button组件是否不必要的开启了动画(Button上只有透明图,但开了点击颜色切换的动画) | 点击动画会触发ReBatch |
检查是否有不带UI元素的节点挂了CanvasRenderer) | 携带CanvasRenderer的节点状态发生变化会导致ReSort和ReBatch |
把比较独立且层级简单的动态ui,采用动静分离,放到一个独立的Canvas下。注:动静分离可以减少ReBatch发生时的平均耗时,但是无法完全避免尖刺的出现,所以如果是循环播放的动画,最好的方法还是换成MeshUI | UI层级简单放到独立的Canvas可以减少rebatch的耗时,但是如果层级复杂或和其它UI耦合比较严重就没办法移出 |
耦合比较严重或层级结构复杂的动态UI,结合实际情况考虑是否替换成MeshUI | 高频改变UI状态会产生耗时峰值 |
Animator中有修改被自动布局组件所控制的UI坐标、大小的,需要和自动布局组件计算的结果保持一致 | 两者数值冲突会导致不断修改Layout,从而持续产生重建的开销 |
经过这些修改,局内UI重建导致的耗时峰值少了很多:
由此总结出以后做局内UI的一些注意事项:
1.Canvas尽量不嵌套
2.战斗中需要切换显隐状态,使用SetLocalScale或CanvasGroup的Alpha(MeshUI除外)
3.带动画或需要频繁更新状态的UI,用SpriteRenderer或UIMeshImage替代
4.动画与自动布局组件计算的数值要保持一致,如果无法保持一致,可能要考虑变更做法
检测工具
在优化重建的过程中,做了几个检测工具以便更快的查出问题: Button动画检测,检测Button不必要的开启动画 CanvasRenderer检测,检测无UI显示组件却挂了CanvasRenderer Resort监测,运行时统计节点开启和关闭引发Resort的次数 和UI的同学同步了这个事,给他们介绍了相关注意事项及工具的用法
DrawCall
其实现在的CPU,DrawCall一般影响不会太大,不过对性能比较敏感的场景,比如局内战斗,还是能省则省。
1.使用动态图集
动态图集就是指在运行过程中动态生成一张贴图,把UI所需要的图片放到这一张贴图里,这样可以有效地减少DrawCall,另外也可以减少游戏内加载图集的数量,减少内存占用。关于动态图集的实现可以查看前面的文章。
2.SpriteRenderer或其他Mesh,隐藏时可以直接SetActive或者设置出视锥体,但不要设置Scale,因为设置Scale不能减少DrawCall。
3.Image、Text不能设置坐标隐藏,因为UI在剔除时是以Canvas为单位,该Canvas下的UI只要有一个还在屏幕内就不会剔除,且改变Z轴会影响深度计算,打断合批,应该使用设置Scale或设置CanvasGroup的Alpha,或者勾选CanvasRenderer组件上的CullTransparentMesh,然后再设置Image或Text的Alpha。
4.自定义的UI组件、shader,可以尝试用顶点信息来设置参数,避免因为SetProperty而导致材质不同,打断合批。具体实现可以参考Image组件。
5.URP管线下,用Mesh做的ui,需要设置Shader参数的,可以尝试用SRPBatcher合批,一个需要避免的坑是SetPropertyBlock和SRPBatcher是不兼容的。
Overdraw
UIOverdraw过高往往是因为叠加的层数过多,此时就需要看是否能进行一些隐藏,这涉及到具体的业务,所以要具体情况具体分析,为了便于找出问题,我做了一个计算Overdraw的工具,将Overdraw的情况数据化,如图所示:
通过这个工具在我们项目中发现了一些渲染顺序的问题,不透明物体要按照从近到远的顺序渲染,否则会产生大量的Overdraw,我们项目是天空的渲染顺序在最前,所以常常会额外产生一倍的Overdraw。
UIShader预热
我们在UI上用了不少自定义shader,主要是技能效果之类的,这些效果在首次出现时Unity需要编译,这会产生一个耗时峰值,造成卡顿,要避免这种卡顿就需要提前对这些Shader进行预热,可以在打包阶段对UIPrefab所引用的材质进行收集遍历,找到所有的关键字组合,将这个信息保存到变体集中,在游戏Loading的时候加载这个变体集。