之前说到优化DrawCall可以降低CPU在渲染上的开销,那么是不是DrawCall越少就一定越好呢?单纯讨论DrawCall的话,确实是,但是如果为了合并DrawCall需要付出的代价很大,就会得不偿失。网格合并(Rebatch)就是UGUI解决DrawCall问题的代价之一。

什么是网格合并

我们知道UGUI绘制UI的过程需要创建Mesh信息,例如一个Image元素在Simple模式下会创建四个顶点,如果我们有100个相同的这样的Image,UGUI底层(Native Code部分)会将这100个Image的Mesh信息合并成一个大的Mesh,用同一个DrawCall发送给CPU,这就是网格合并,是UGUI行之有效的降低DrawCall提高效率的方法。

一切看起来很美好,但是,UI的Mesh信息相对于普通的Mesh来说有一个很大的不同,就是UI的Mesh信息是会变化的,而一旦某个UI元素的Mesh信息发生修改,那么和这个元素合并在一批的整个Mesh都会变成不可用,此时就只能重新合并,颇有种“一块臭肉坏了满锅汤”的意味。

可以根据一个例子来体会下这个过程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class RebatchSimulation : MonoBehaviour
{
    public RectTransform UIElement;
    public RectTransform StaticUIRoot;
    public RectTransform DancingUIRoot;

    const int StaticUINum = 5000;
    const int DancingUINum = 1;

    private List<RectTransform> _dancingElements = new List<RectTransform> ();

    private bool _dancing = false;

    // Start is called before the first frame update
    void Start()
    {
        for (int idx = 0; idx < StaticUINum; ++idx) {
            RectTransform staticUI = GameObject.Instantiate(UIElement, StaticUIRoot);
            staticUI.anchoredPosition = Vector2.right *  (idx - StaticUINum / 2);
        }
        for(int idx = 0; idx < DancingUINum; ++idx) {
            RectTransform dancingUI = GameObject.Instantiate(UIElement, DancingUIRoot);
            dancingUI.anchoredPosition = Vector2.zero;
            _dancingElements.Add(dancingUI);
        }
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space)) {
            _dancing = !_dancing;
        }
        if (_dancing) {
            int dancingRectCount = _dancingElements.Count;
            for (int idx = 0; idx < dancingRectCount; ++idx) {
                RectTransform dancingRect = _dancingElements[idx];
                if (dancingRect != null) {
                    dancingRect.anchoredPosition = new Vector2(Random.Range(-50, 50), Random.Range(-50, 50));
                }
            }
        }
    }
}

我们创建了5000个静止的Image和一个跳舞的Image,跳舞是指我们可以通过按下空格键来切换跳舞状态,当处于跳舞状态时,它会随机出现在一个位置。通过Unity的Profiler可以体会下这种变化:Mesh合并Profiler对比

通过搜索可以发现Canvas.BuildBatch这一项的Call是1,即被调用一次,而在跳舞之前这一项的Call为0。

可以通过这种方式测试下常见的对UI元素的修改方式是否导致Rebatch:

修改内容 是否导致Rebatch
anchorPosition
sizeDelta
localPosition
localRotation
localScale
SetActive
color
SourceImage
raycastTarget ×
text

可以看到大部分对于UI的操作都会引起Rebatch,这可能也是很多人将Rebuild和Rebatch混为一谈的原因,毕竟一旦对UI元素进行了操作,Rebuild和Rebatch几乎都会发生。但是他们本质上是不同的东西,Rebuild发生在C#层面,是某个具体的元素重新生成Mesh信息,性能消耗受发生Rebuild的元素多少影响,而Rebatch发生在Native Code层面,是合并后的Mesh信息需要重组,其性能消耗受发生次数和涉及Mesh复杂程度影响。

建议

可以说只要UI元素需要动1,Rebatch就是不可避免的,我们能做的只是将他的开销降低,也就是降低需要动的UI元素经过Batch后的Mesh的复杂程度,这就是老生常谈的“动静分离”的本质。以我们的RebatchSimulation为例,这里把5000个不动的元素和1个动的元素放在一个Canvas下,也就是UGUI会将这5001个UI元素的Mesh信息合并成一个大的Mesh处理,这时其中一个元素动了,这个大的Mesh就需要重新组建,造成严重的性能开销。优化的方法也很明显,就是将它们分离到不同的Canvas下,由于UGUI渲染是以Canvas为单位,动的元素就不会影响到其他Canvas里面静的元素。

注意

Rebatch组建Mesh是以Canvas为单位而非DrawCall,也就是说一个UI元素动了,是以Canvas为单位去Rebatch,把动的元素分隔到单独DrawCall并不能达到动静分离的目的。


  1. 这里的动并非字面意义的动,而是指对UI元素会导致Rebatch的操作。 ↩︎