前言

在 Unity 中,Rebuild 和 Rebatch 是优化 UI 渲染性能的重要概念。了解它们的原理可以帮助开发者优化游戏的性能,尤其是在使用 Unity 的 UI 系统时。

Rebuild

什么是Rebuild

当发生一些脏标记的行为时,被标记为脏的对象需要进行重新计算或渲染

图形组件Rebuild

在渲染Canvas前一帧,会去判断Graphic是否被标记为脏标记,如果是顶点脏标记就会去重建网格,如果是材质被标记为脏那么就会去重建材质

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
//重建方法
public virtual void Rebuild(CanvasUpdate update)
{
if (canvasRenderer == null || canvasRenderer.cull)
return;

switch (update)
{
case CanvasUpdate.PreRender:
//顶点被标记为脏(true)
if (m_VertsDirty)
{
//更新网格
UpdateGeometry();
m_VertsDirty = false;
}
if (m_MaterialDirty)
{
//更新材质
UpdateMaterial();
m_MaterialDirty = false;
}
break;
}
}


//网格刷新方法
private void DoMeshGeneration()
{
if (rectTransform != null && rectTransform.rect.width >= 0 && rectTransform.rect.height >= 0)
//更新网格数据
OnPopulateMesh(s_VertexHelper);
else
//清除网格数据
s_VertexHelper.Clear(); // clear the vertex helper so invalid graphics dont draw.

var components = ListPool<Component>.Get();
GetComponents(typeof(IMeshModifier), components);
//https://zhuanlan.zhihu.com/p/340601601 可以看该链接去看这方法的作用
//简介就是继承该接口的组件(Shadow Outline)也行进行刷新网格信息
for (var i = 0; i < components.Count; i++)
((IMeshModifier)components[i]).ModifyMesh(s_VertexHelper);

ListPool<Component>.Release(components);
//将网格数据赋值给workerMesh
s_VertexHelper.FillMesh(workerMesh);
//存储数据供渲染
canvasRenderer.SetMesh(workerMesh);
}



//材质更新代码
protected virtual void UpdateMaterial()
{
if (!IsActive())
return;
//将数据赋值给了canvasRenderer
canvasRenderer.materialCount = 1;
canvasRenderer.SetMaterial(materialForRendering, 0);
canvasRenderer.SetTexture(mainTexture);
}

布局组件Rebuild

简介
当组件发生一些脏标记行为时,对象就需要进行重建,例如ScrollRect组件的RectTransform组件参数发生了改变就会引发Rebuild

具体实现
拿ScrollRect组件举例,当修改了RectTransform的参数时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
protected override void OnRectTransformDimensionsChange()
{
//当该对象的RectTransform参数被修改时调用
SetDirty();
}
//脏标记方法
protected void SetDirty()
{
if (!IsActive())
return;
//通过LayoutRebuilder类去把该对象收集到CanvasUpdateRegistry类中去重建
LayoutRebuilder.MarkLayoutForRebuild(rectTransform);
}

Rebuild是怎么被触发的

Graphic对象一共有两种脏标记,以下我只写一些常见引起脏标记行为
顶点被标记为脏

  • 当修改fillAmount的数值时

  • 修改了RectTransform

  • 修改Image 的color

  • 禁用或启用SetActive(两则都会标记为脏)

  • 设置Image的SetNativeSize(两则都会标记为脏)

  • 替换Sprite(两则都会标记为脏)

  • 材质被标记为脏

    • 替换材质

    • 修改RectTransform

  • ILayoutGroup(ScrollRect,LayoutGroup)对象发生脏标记
    布局脏标记

    • Graphic对象(Image)的禁用或激活
    • Text对象的字体大小修改或布局修改

触发收集的脏元素

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
CanvasUpdateRegistry.cs

//收集所有的布局或图形脏对象
private readonly IndexedSet<ICanvasElement> m_LayoutRebuildQueue = new IndexedSet<ICanvasElement>();
private readonly IndexedSet<ICanvasElement> m_GraphicRebuildQueue = new IndexedSet<ICanvasElement>();

//被注册的方法,被底层调用
private void PerformUpdate()
{
UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Layout);
CleanInvalidItems();

m_PerformingLayoutUpdate = true;

m_LayoutRebuildQueue.Sort(s_SortLayoutFunction);
var layoutRebuildQueueCount = m_LayoutRebuildQueue.Count;

for (int i = 0; i <= (int)CanvasUpdate.PostLayout; i++)
{
UnityEngine.Profiling.Profiler.BeginSample(m_CanvasUpdateProfilerStrings[i]);

for (int j = 0; j < layoutRebuildQueueCount; j++)
{
var rebuild = m_LayoutRebuildQueue[j];
try
{
if (ObjectValidForUpdate(rebuild))
//重建布局元素
rebuild.Rebuild((CanvasUpdate)i);
}
catch (Exception e)
{
Debug.LogException(e, rebuild.transform);
}
}
UnityEngine.Profiling.Profiler.EndSample();
}

for (int i = 0; i < layoutRebuildQueueCount; ++i)
m_LayoutRebuildQueue[i].LayoutComplete();

m_LayoutRebuildQueue.Clear();
m_PerformingLayoutUpdate = false;
UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Layout);
UISystemProfilerApi.BeginSample(UISystemProfilerApi.SampleType.Render);

// now layout is complete do culling...
UnityEngine.Profiling.Profiler.BeginSample(m_CullingUpdateProfilerString);
ClipperRegistry.instance.Cull();
UnityEngine.Profiling.Profiler.EndSample();

m_PerformingGraphicUpdate = true;

var graphicRebuildQueueCount = m_GraphicRebuildQueue.Count;
for (var i = (int)CanvasUpdate.PreRender; i < (int)CanvasUpdate.MaxUpdateValue; i++)
{
UnityEngine.Profiling.Profiler.BeginSample(m_CanvasUpdateProfilerStrings[i]);
for (var k = 0; k < graphicRebuildQueueCount; k++)
{
try
{
var element = m_GraphicRebuildQueue[k];
if (ObjectValidForUpdate(element))
//重建图形元素
element.Rebuild((CanvasUpdate)i);
}
catch (Exception e)
{
Debug.LogException(e, m_GraphicRebuildQueue[k].transform);
}
}
UnityEngine.Profiling.Profiler.EndSample();
}

for (int i = 0; i < graphicRebuildQueueCount; ++i)
m_GraphicRebuildQueue[i].GraphicUpdateComplete();

m_GraphicRebuildQueue.Clear();
m_PerformingGraphicUpdate = false;
UISystemProfilerApi.EndSample(UISystemProfilerApi.SampleType.Render);
}

Rebatch 原理

Rebatch 是指 Unity 在渲染过程中重新组合批次的过程。批次(Batch)是指将多个渲染命令合并成一个,以减少 CPU 与 GPU 之间的通信开销。UI 元素的批次合并可以显著提高渲染性能,因为它减少了绘制调用(Draw Call)的数量。

什么是Rebatch

将多个对象合并渲染
https://zhuanlan.zhihu.com/p/340480771
作用
Rebatch(UI合批),当项目中的Drawcall数量过多时,就需要通过Rebatch来减少UIDrawcall的次数,常用的方法就是图集减少Drawcall的链接教程

Rebatch是怎么被触发的

Unity5.2后Rebatch的操作就放在了多线程操作(具体什么版本不清楚),所以Rebatch被触发的操作也找不到,

触发 Rebatch 的常见情况:

  1. 改变材质或纹理。
  2. 修改 UI 元素的透明度。
  3. 更改 Z 轴顺序(渲染顺序)。
  4. 动态添加或移除 UI 元素

优化

  • 减少Rebuild的消耗

    • 引起元素Rebuild的操作就是发生了脏标记行为,所以减少脏标记行为就能减少Rebuild的消耗,
    • 当Canvas被标记为脏时就会重新计算(UWA:Canvas下的元素会先合并一个Sub-Mesh,最后合并成一个Canvas为单位的Mesh)
  • 减少Rebatch的消耗

    • Rebatch会将Canvas下的元素进行提交渲染并缓存,直到Canvas下的某个元素发生了脏标记,那么就会去重新计算,所以减少脏标记的行为就可以减少Rebatch的消耗。
  • 动静分离

    • Rebatch是以Canvas为单位提交的,所以当出现经常发生脏标记的元素可以单独放在一个Cnavas或Sub-Canvas下。这样可以减少Rebatch的消耗。

总结

  • Rebuild 是重新生成 UI 几何数据和布局的过程。通过减少对 UI 元素属性的频繁修改和批量更新 UI 元素,可以优化 Rebuild。
  • Rebatch 是重新组合批次的过程。通过使用相同的材质和纹理、减少透明度变化、合理组织 UI 层级结构,可以优化 Rebatch。

通过了解和优化 Rebuild 和 Rebatch,可以显著提高 Unity 游戏中 UI 系统的渲染性能。

源码角度分析Rebuild和Rebatch_rebuild和rebatch原理-CSDN博客