一、界面制作
1.在UI界面中,因为一个Canvas下的所有UI元素都是合在一个Mesh中的,过大的Mesh在更新时开销很大,所以一般建议每个较复杂的UI界面,都自成一个Canvas(可以是子Canvas),在UI界面很复杂时,甚至要划分更多的子Canvas。同时还要注意动态元素和静态元素的分离,因为动态元素会导致Canvas的mesh的更新。最后,Canvas又不能细分的太多,因为会导致Draw Call的上升。
2.UWA性能检测报告中的Shared UI Mesh:
Shared UI Mesh作为静态全局变量,由底层直接维护,其大小与当前场景中所有激活的UI元素所生成的网格数相关。一般来说当界面上UI元素较多,或者文字较多时该值都会较高,在使用UI/Effect/shadow和UI/Effect/Outline时需要注意该值,因为这两个Effect会明显增加文字所带来的网格数。
3.打包图集:设计UI时要考虑重用性,如一些边框、按钮等,这些作为共享资源,放在1~3张大图集中,称为重用图集;其它非重用UI按照功能模块进行划分,每个模块使用1~2张图集,为功能图集;对于一些UI,如果同时用到功能图集与重用图集,但是其功能图集剩下的“空位”较多,则可以考虑将用到的重用图集中的元素单独拎出来,合入功能图集中,从而做到让UI只依赖于功能图集。也就是通过一定的冗余,来达到性能的提升。
4.把Packing Tag相同的源纹理文件,打到同一个AssetBundle中(设置一样的AssetBundle Name),从而避免Atlas的冗余。同时这样打包可以让依赖它的Canvas的打包更加自由,即不需要把依赖它的Canvas都打在一个AssetBundle中,在更新时直接更新Atlas所在的AssetBundle即可。
5.ScrollRect在滚动的时候,会产生Canvas.SendwillRenderCanvases:
ScrollRect在滚动时,会产生OnTransformChanged的开销,这是UI元素在移动时触发的,但通常这不会触发Canvas.SendWillRenderCanvases。
如果观察到Canvas.SendWillRenderCanvases耗时较高,可以检查下ScrollRect所在的Canvas是否开启了Pixel Perfect的选项,该选项的开启会导致UI元素在发生位移时,其长宽会被进行微调(为了对其像素),而ScrollRect中通常有较多的UI元素,从而产生较高的Canvas.SendWillRenderCanvases开销。因此可以尝试关闭Pixel Perfect看效果是否可以接受,或者尝试在滚动过程中暂时关闭Pixel Perfect等方式来消除其开销。
6.在整体游戏内存压力不大的情况下,常用界面可以预先在加载在场景(Scene)中,对加载完成的界面进行缓存,确保下一次打开时不会再卡顿。
7.少用mask:Mask对于uGUI性能来说是噩梦一般的存在,因为很可能因为这个东西,导致Drawcall数量成倍增长。Mask实现的具体原理是一个Drawcall来创建Stencil mask(来做像素剔除),然后画所有子UI,再在最后一个Drawcall移掉Stencil mask。这头尾两个Drawcall无法跟其他UI操作进行Batch,所以表面上看加个Mask就会多2个Drawcall,而且Mask中的UI元素无法与其他batch,所以很多原本可以合并的UI就无法合并了,从而增加DrawCall
8.关注相邻对象的合批问题,通过重新排列可绘制对象的顺序、调整对象的位置以消除不可见的重叠空间等方式减少DC
9.合理利用子Canvas处理一个界面过大、动静分离等问题,但要注意不同Canvas不会合批
10.关闭不需要的Raycast Target,仅在必须接收指针事件的UI组件上启用“ Raycast Target”设置。Graphic Raycaster会检测将“ Raycast Target”设置为true的所有Graphic组件。对于每个Raycast Target,Raycaster都会执行一组测试。
二、网格
1.如果修改的是Image组件上的Color属性,其原理是修改顶点色,因此是会引起网格的Rebuild的(即Canvas.BuildBatch操作,同时也会有Canvas.SendWillRenderCanvases的开销)。而通过修改顶点色来实现UI元素变色的好处在于,修改顶点色可以保证其材质不变,因此不会产生额外的Draw Call。
2.在UI的默认Shader中存在一个Tint Color的变量,正常情况下,该值为常数(1,1,1),且并不会被修改。如果是用脚本访问Image的Material,并修改其上的Tint Color属性时,对UI元素产生的网格信息并没有影响,因此就不会引起网格的Rebuild。但这样做因为修改了材质,所以会增加一个Draw Call。
3.在 UGUI 中,Batch是以Canvas为单位的,即在同一个Canvas下的UI元素最终都会被Batch到同一个Mesh中。而在Batch前,UGUI会根据这些UI元素的材质(通常就是Atlas)以及渲染顺序进行重排,在不改变渲染结果的前提下,尽可能将相同材质的UI元素合并在同一个SubMesh中,从而把DrawCall降到最低。而Batch的操作只会在UI元素发生变化时才进行,且合成的Mesh越大,操作的耗时也就越大。
因此,尽可能把频繁变化(位置,颜色,长宽等)的UI元素从复杂的Canvas中分离出来,从而避免复杂的Canvas频繁重建。在UGUI中,网格的更新或重建(为了尽可能合并UI部分的DrawCall)是以Canvas为单位的,且只在其中的UI元素发生变动(位置、颜色等)时才会进行。因此,将动态UI元素与静态UI元素分离后,可以将动态UI元素的变化所引起的网格更新或重建所涉及到的范围变小,从而降低一定的开销。而静态UI元素所在的Canvas则不会出现网格更新和重建的开销。
4.多人同屏的时候,人物移动会使得头顶上的名字Mesh重组,从而导致较为严重的卡顿:
如果是用UGUI开发的,当头顶文字数量较多时,确实很容易引起性能问题,可以考虑从以下几点入手进行优化:
尽可能避免使用UI/Effect,特别是Outline,会使得文本的Mesh增加4倍,导致UI重建开销明显增大;
拆分Canvas,将屏幕中所有的头顶文字进行分组,放在不同的Canvas下,一方面可以降低更新的频率(如果分组中没有文字移动,该组就不会重建),另一方面可以减小重建时涉及到的Mesh大小(重建是以Canvas为单位进行的);
降低移动中的文字的更新频率,可以考虑在文字移动的距离超过一个阈值时才真正进行位移,从而可以从概率上降低Canvas更新的频率。
三、界面切换:
1.把被覆盖的界面 SetActive(False),但发现后续 SetActive(True) 的时候会有 GC.Alloc 产生。这种情况下,希望既降低 Batches 又降低 GC Alloc 的话
可以尝试通过添加一个 Layer 如 OutUI, 且在 Camera 的 Culling Mask 中将其取消勾选(即不渲染该 Layer)。从而在 UI 界面切换时,直接通过修改 Canvas 的 Layer 来实现“隐藏”。但需要注意事件的屏蔽,禁用动态的 UI 元素等等。这种做法的优点在于切换时基本没有开销,也不会产生多余的 Draw Call,但缺点在于“隐藏时”依然还会有一定的持续开销(通常不太大),而其对应的 Mesh 也会始终存在于内存中(通常也不太大)。
2.GC Alloc 并不是由Instantiate 直接引起的,而是因为被实例化出来的组件会进行 OnEnable 操作,而在 OnEnable 操作中产生了 GC,
因此,我们不建议通过 Instantiate/Destroy 来处理切换频繁的 UI 界面,而是通过 SetActive(true/false),甚至是直接移动 UI 的方式,以避免反复地造成堆内存开销。
3.全屏界面下的3d对象,可以通过隐藏3d对象父节点或隐藏3d相机减少消耗。对于能看到一小部分3d对象的界面,可以通过屏幕截图的方式保存一个备份并显示在界面底部,从而释放底下的3d资源。
4.合并图片(例如道具框和道具底图),透明度为0的对象任然会消耗渲染资源,这种对象尽量隐藏掉。
四、加载相关
加载UI预制的时候,如果把特效放到预制里,会导致加载非常耗时:
UI和特效(粒子系统)的加载开销在多数项目中都占据较高的CPU耗时。UI界面的实例化和加载耗时主要由以下几个方面构成:
纹理资源加载耗时,UI界面加载的主要耗时开销,因为在其资源加载过程中,时常伴有大量较大分辨率的Atlas纹理加载,我们在之前的Unity加载模块深度分析之纹理篇有详细讲解。对此,我们建议研发团队在美术质量允许的情况下,尽可能对UI纹理进行简化,从而加快UI界面的加载效率。
UI网格重建耗时,UI界面在实例化或Active时,往往会造成Canvas(UGUI)或Panel(NGUI)中UIDrawCall的变化,进而触发网格重建操作。当Canvas或Panel中网格量较大时,其重建开销也会随之较大。
UI相关构造函数和初始化操作开销,这部分是指UI底层类在实例化时的ctor开销,以及OnEnable和OnDisable的自身开销。
上述2和3主要为引擎或插件的自身逻辑开销,因此,我们应该尽可能避免或降低这两个操作的发生频率。我们的建议如下:
在内存允许的情况下,对于UI界面进行缓存。尽可能减少UI界面相关资源的重复加载以及相关类的重复初始化;
根据UI界面的使用频率,使用更为合适的切换方式。比如移进移出或使用Culling Layer来实现UI界面的切换效果等,从而降低UI界面的加载耗时,提升切换的流畅度。
对于特效(特别是粒子特效)来说,我们暂时并没有发现将UI界面和特效耦合在一起,其加载耗时会大于二者分别加载的耗时总和。因此,我们仅从优化粒子系统加载效率的角度来回答这个问题。粒子系统的加载开销,就目前来看,主要和其本身组件的反序列化耗时和加载数量相关。对于反序列化耗时而言,这是Unity引擎负责粒子系统的自身加载开销,开发者可以控制的空间并不大。对于加载数量,则是开发者需要密切关注的,因为在我们目前看到的项目中,不少都存在大量的粒子系统加载,有些项目的数量甚至超过1000个,如下图所示。因此,建议研发团队密切关注自身项目中粒子系统的数量使用情况。一般来说,建议我们建议粒子系统使用数量的峰值控制在400以下。
unload(false)卸载AssetBundle并不会销毁其加载的资源 ,是必须调用 Resources.UnloadUnusedAssets才行。
五、字体
Font.CacheFontForText主要是指生成动态字体Font Texture的开销, 一次性打开UI界面中的文字越多,其开销越大。如果该项占用时间超过2s,那么确实是挺大的,这个消耗也与已经生成的Font Texture有关系。简单来说,它主要是看目前Font Texture中是否有地方可以容下接下来的文字,如果容不下才会进行一步扩大Font Texture,从而造成了性能开销。