简介主要性能消耗点

  • rebatch 

        Rebatch发生在C++层面,是指Canvas分析UI节点生成最优批次的过程,节点数量过多会导致算法(贪心策略)耗时较长。对应SetVerticesDirty,当一个canvas中包含的mesh发生改变时就触发,例如SetActive、transform的改变、 颜色改变、文本内容改变等等,canvas独立处理,互相不影响。消耗在对meshes按照深度和重叠情况排序、共享材质的检测等。

Batch以Canvas为单位,同一个Canvas下的UI元素最终都会被Batch到一个Mesh中。Batch前,UGUI根据UI材质以及渲染顺序重排,在不改变渲染结果的前提下,尽可能将相同材质的UI元素合并在同一个SubMesh中,以减少DC。Batch只在UI元素发生变化时进行,合成的Mesh越大,耗时越大。重建对Canvas下所有ui元素生效,不论是否修改过。

5.2之后底层是多线程的,考虑到现在手机上都是多核,这部分消耗可能会小很多。

  • rebuild

        Rebuild发生在C#层面,是指UGUI库中layout组件调整RectTransform尺寸、Graphic组件更新Material,以及Mask执行Cull的过程,耗时和发生变化的节点数量基本呈线性相关。

只有LayoutGroup的直接子节点,并且是 Graphic类型的(比如 Image 和 Text)会触发SetLayoutDirty。

Graphic改变的原因包括,基本的大小、旋转以及文字的变化、图片的修改等等,对应SetMaterialDirty。

rebatch与rebuild优化

  • Canvas动静分离:保持不变的放到一个Canvas下,对于要修改颜色(原理是修改顶点颜色触发Rebatch),文本内容的放到一个Canvas下
  • 减少节点层次和数量,合批计算量小,速度快
  • 少用layout,简单的布局RectTransform代替

针对组件的优化

  • Button或Image接收事件不显示的时候, 这些image虽然被alpha被设置为0不可见,但是DrawCall依然存在,重写MaskableGraphic中OnPopulateMesh方法,清除顶点不绘制。
using UnityEngine;

using System.Collections;

namespace UnityEngine.UI

{

    public class Empty4Raycast : MaskableGraphic

    {
        protected Empty4Raycast()

        {
            useLegacyMeshGeneration = false;
        }

        protected override void OnPopulateMesh(VertexHelper toFill)

        {
            toFill.Clear();
        }

    }

}
  • 取消不必要RaycastTarget。所有UGUI组件在创建时都默认开启了Raycast Target这一选项,实际上是为接受事件响应做好了准备。而事实上,大部分比如Image、Text类型的UI组件是不会参与事件响应的,但仍然会在鼠标/手指划过或悬停时参与轮询,通过模拟射线检测判断UI组件是否被划过或悬停,造成不必要的耗时。尤其在项目中UI组件比较多时,关闭不参与事件响应的组件的Raycast Target设置,可以有效降低EventSystem.Update()耗时
  • Mask与RectMask2D使用 以下是结论,最后会有示例工程参考

        1.多个Mask之间可以进行合批

        2.Mask内外不能进行合批.

        3.RectMask2D本身不产生drawcall.

        4.不同RectMask2D的子对象不能合批.

显示\隐藏优化

控制UI显示\隐藏时,大部分会想到SetAcive(true/false)。但实际上Text.OnEnable()性能消耗大,它会对初始化文本网格的信息(每个文字所在的网格顶点,UV,顶点色等等属性),而这些信息都是储存在数组中(即堆内存中),文本越多,堆内存开销越大,相应GC就产生了。所以如果频繁可以考虑修改设置层级Layer达到不显示的目的,或添加CanvasGroup,控制alpha与blocksRaycasts(参考)

Draw Call 方面优化

首先了解渲染规则:

两个UI控件能合批的基本条件是这两个控件使用的材质球(Shader)和贴图要完全相同。比如Text和Image默认使用的材质球都是UI/Default,但是两者使用的贴图不同,所以注定Text和Image无法合批。

UGUI中完整的合批流程(规则)如下。
首先我们要明确UGUI中Canvas下可以嵌套子Canvas,但是合批是以Canvas(不包含子Canvas)为单位的(子Canvas会是另外一个批次了)。除此之外,合批的操作是在子线程完成的。
①既然合批是以Canvas为单位,第一步自然就是把所有Canvas给找出来,然后剔除掉不必渲染的Canvas(透明度为0,长宽为0,在RectMask2D控件下,且在RectMask2D的区域外)
②然后计算Canvas下各UI控件的深度值Depth(需要注意的是Image的属性里面也有个depth,两者不是同一个东西)
③Depth的计算规则如下:

按照Hierarchy中从上往下的顺序依次遍历Canvas下所有UI元素
对于当前的UI元素CurrentUI
1、如果CurrentUI不渲染,则Depth = -1
2、如果CurrentUI要渲染,但CurrentUI下面没有其他UI元素与其相交,则Depth = 0
3、如果CurrentUI要渲染,下面只有一个UI元素(LowerUI)与其相交,且CurrentUI与LowerUI可以合批(材质和贴图完全相同),则CurrentUI.Depth = LowerUI.Depth;如果两者不能合批,CurrentUI.Depth= LowerUI.Depth + 1
4、如果CurrentUI要渲染,下面有n个元素与其相交,则按照步骤iii,分别计算出n个Depth(Depth_1、Depth_2、Depth_3…),然后CurrentUI.Depth取其最大值,即CurrentUI.Depth = max(Depth_1, Depth_2, Depth_3,…)
上面步骤中的“下面”和“相交”要明确下意思,这两个概念很重要。
CurrentUI下面的UI,指Hierarchy面板中,在CurrentUI之上的元素。

两个UI元素相交,是指这两个元素的网格有相交(有重叠部分),一定要注意不是两个元素的Rect区域相交。

在计算相交时,由于要遍历所有UI元素和已计算的底层UI元素(平方复杂度),源码中使用分组计算包围盒矩形的方法加快计算,即16个UI元素为一组计算Group 网格Rect,检查是否与底层UI元素相交时,先计算是否与底层Group相交,如果相交再与Group中的元素做判定。
④各个UI的Depth计算完毕后,依次按照Depth、material ID、texture ID、RendererOrder(即UI层级队列顺序,即Hierarchy面板上的顺序)排序(条件的优先级依次递减,且均为从小到大排序)。然后剔除Depth = -1的UI元素,得到Batch前的UI 元素队列,这个队列被称之为VisiableList。
上面这段话有些地方可能没太说清楚,解释一下排序:
先按Depth从小到大的顺序排序
Depth排完之后,Depth相同的元素再按material ID从小到大排序
material ID排完之后,material ID相同的元素再按texture ID从小到大排序
textrure ID排完之后,textrure ID相同的元素最后再按在Hierarchy上的顺序排序(Hierarchy越上面的越在队列前面)
⑤得到VisiableList之后,判断VisiableList中相邻的元素是否能够合批(相同的材质和贴图)。需要注意这里不再考虑Depth是否相同,只要两个元素相邻然后材质和贴图相同,即使两个元素的Depth不相同,这两个元素也能合批。然后一个批次一个批次的合并网格,提交GPU进行渲染。
除此之外,需要注意的是,合批是将同一Canvas下多个UI的网格合并在一起,如果其中任何一个元素的材质、网格顶点、位置(Transform)甚至颜色或者在该Canvas下动态创建或删除UI元素都将导致该Canvas重新计算合批(需要注意的是仅仅会影响这一个Canvas,子Canvas或父Canvas以及其他Canvas不会重新计算),重新生成新的网格,这个重新计算生成网格的过程被称为rebuild。所以,这也是为什么做UI提倡动静分离(动态部分和静态部分分别用不同的Canvas),层级尽量减少(层级多了,重新计算更耗时)的原因。

合批的规则搞清楚了,但彻底弄懂还需要练习一下。我这里专门挑选了几个例子,跟着做一遍应该能大大加深理解了。
在开始之前,我们得先知道material Id和texture Id怎么获取到,其实很简单,直接GetInstanceID()就行了。

// materialId
image.material.GetInstanceID()
// textureId
image.mainTexture.GetInstanceID()


3.3.2 合批规则示例1

如图,三张Image使用的材质都是UI/Default,Image1和Imge3使用的贴图的texture Id = 13188,Image2的texture Id = -1136。
现在,请分析它们三者的合批情况和渲染顺序(先渲染哪张图,再渲染哪张图)?
①首先,先分别计算三张图片的Depth(还记得Depth是怎么计算的吗?忘了再去上面看看)。

Image1下面没有其他UI,所以Image1的Depth = 0
(这里我再提醒一下,image有个depth字段,我们计算出的合批的Depth和image的depth字段不是同一个东西,虽然使用的变量名一样但不是同一个东西,千万不要搞混了)
Image2下面有Image1,但是Image1没有与Image2相交,所以Image2的Depth = 0
Imge3下面有Imag1和Image2,分别计算Image3下只有其中一个元素时Imge3的Depth,然后取其最大值。
Image3与Imge1不相交,所以Image3的Depth = Image1的Depth = 0;
Image3与Imge2相交,所以Image3的Depth = Image2的Depth + 1 = 1;
然后取其最大值,所以Image3的Depth = 1。
UI    合批的Depth

UI合批的Depth
Image10
Image20
Image31


②然后按照依次Depth、material Id、texture Id、hierarchy sort order进行升序排序,得到VisiableList。

先按Depth排序,顺序为Image1》Image2》Imge3;
再按material Id排序,由于三个Image的材质相同(即material Id相同),则顺序不变,仍然是Image1》Image2》Imge3;
然后再按texture Id排序,由于Image2.textureId(-1136) < Image1.textureId(13188),所以Image1和Image2要进行交换,则顺序为Image2》Image1》Imge3;
所以VisiableList = {Image2, Imgae1, Image3}。
③最后判断相邻元素是否能否合批,计算合批次数。
Image2和Image1材质相同但贴图不同,所以Image2和Image1不能合批;
Image1和Image3材质和贴图均相同,所以Image1和Image3可以合批(这里需要注意的是,虽然Image1和Image3的Depth不相同,但是到这一步是不再考虑这个问题的);
也就是说Image2单独绘制,Image1和Image3合批再绘制一次。
我们去Profiler UI看看咱们的分析是否正确。

可以看到咱们的分析是正确的。

3.3.3 合批规则示例2

如图,三张Image使用的材质都是UI/Default,Image1和Imge3使用的贴图的texture Id = -1136,Image2的texture Id = 13188。
现在,请分析它们三者的合批情况和渲染顺序(先渲染哪张图,再渲染哪张图)?
①首先,先分别计算三张图片的Depth。

Image1下面没有其他UI,所以Image1的Depth = 0
Image2下面也没有其他UI,所以Image2的Depth = 0
Imge3下面有Imag1和Image2,分别计算Image3下只有其中一个元素时Imge3的Depth,然后取其最大值。
Image3与Imge1不相交,所以Image3的Depth = Image1的Depth = 0;
Image3与Imge2相交,所以Image3的Depth = Image2的Depth + 1 = 1;
然后取其最大值,所以Image3的Depth = 1。
UI    合批的Depth
I

UI合批的Depth
Image10
Image20
Image3

②然后按照依次Depth、material Id、texture Id、hierarchy sort order进行升序排序,得到VisiableList。

先按Depth排序,顺序为Image1》Image2》Imge3;
再按material Id排序,由于三个Image的材质相同(即material Id相同),则顺序不变,仍然是Image1》Image2》Imge3;
然后再按texture Id排序,由于Image2.textureId(13188) > Image1.textureId(-1138),所以顺序不变,仍然是Image1》Image2》Imge3;
所以VisiableList = {Image1, Imgae2, Image3}。
③最后判断相邻元素是否能否合批,计算合批次数。
Image1和Image2材质相同但贴图不同,所以Image1和Image2不能合批;
Image2和Image3材质相同但贴图不同,所以Image2和Image3也不能合批;
也就是说Image1、Image2、Image3都是单独绘制,共三个批次,绘制顺序为Image1、Image2、Image3。
我们去Profiler UI看看咱们的分析是否正确。

可以看到咱们的分析是正确的。

了解规则后就可以做好排版尽量去合批了。

示例工程

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐