一、 Rebatch
在 Unity UGUI 的开发中,性能优化是每个团队都会面临的课题。当 UI 界面变得复杂、节点数量增多时,卡顿和掉帧往往随之而来。而在所有性能问题中,Rebatch 是核心的性能消耗点之一,也是开发者经常接触但又容易误解的概念。
1、什么是 Rebatch?
1.1 基本概念
在 UGUI 中,UI 的渲染可以分为两个核心步骤:
- Rebatch:发生在 C++ 层面,是 Canvas 分析 UI 节点、生成最优批次(渲染指令)的过程
- Rebuild:发生在 C# 层面,是 Layout 组件调整 RectTransform、Graphic 组件更新 Mesh 和 Material 的过程
简单来说,Rebatch 就是合批计算。Canvas 负责把它包含的所有 UI 元素的 Mesh 合批,生成合适的渲染命令发送给 Unity 的图形系统。这个过程在 Native C++ 代码中完成,被称为一次 Rebatch 或 Batch Build。
1.2 Batch 的缓存机制
Batch 的结果会被缓存并复用,直到这个 Canvas 被标记为 Dirty。当 Canvas 中某一个 UI 元素的网格发生改变时,Canvas 就会被标记为 Dirty 状态,从而触发下一次 Rebatch。
1.3 Rebatch 的本质目的
Rebatch 的本质目的是减少 DrawCall。从 CPU 把数据发送到显卡相对较慢,合批是为了一次性发送尽可能多的数据。Batch 以 Canvas 为单位,同一个 Canvas 下的 UI 元素最终都会被 Batch 到一个 Mesh 中。Batch 前,UGUI 会根据 UI 材质以及渲染顺序进行重排,在不改变渲染结果的前提下,尽可能将相同材质的 UI 元素合并在同一个 SubMesh 中。
2、Rebatch 的触发条件
当一个 Canvas 中包含的网格(Mesh)发生改变时,就会触发 Rebatch。对应到代码层面,就是调用了 SetVerticesDirty。
触发 Rebatch 的常见操作包括:
| 操作类型 | 具体示例 |
|---|---|
| 显示/隐藏 | SetActive 切换 |
| 变换改变 | Transform 的位置、旋转、缩放变化 |
| 颜色改变 | 修改 Image 的 Color 属性 |
| 文本改变 | Text 组件的内容变化 |
| 图片改变 | Image 的 Sprite 替换 |
需要特别注意:Rebatch 对 Canvas 下所有 UI 元素生效,不论是否被修改过。这意味着,即使只是一个小小的文本变化,整个 Canvas 下所有 UI 元素都会参与 Rebatch 计算。
3、Rebatch 的性能特点
3.1 算法复杂度
Rebatch 的核心算法是贪心策略。它需要:
- 对所有 Mesh 按照深度和重叠情况进行排序
- 检测共享材质
- 生成最优的批次
组合节点数量越多,这个算法的耗时就越长。合成的 Mesh 越大,耗时也越大。
3.2 多线程优化
好消息是,Unity 5.2 之后对 Canvas.BuildBatch 流程做了重要优化。在 CPU 超过一个核心的情况下,Unity 将 Canvas.BuildBatch 流程放在主线程之外,使用多线程进行计算。
由于不再占用主线程的时间,BuildBatch 的消耗在 Profiler 中已不易察觉。但这并不意味着 Rebatch 可以完全忽略------多线程只是转移了主线程的压力,计算工作量本身并未减少。如果每隔几帧就要 Rebatch 一次,CPU 耗时仍然是相当可观的。
4、Rebatch 的优化策略
针对 Rebatch 的优化,核心思路是让变化的影响最小化,让合批的效率最大化。
4.1 动静分离(最核心的手段)
原理 :将频繁变动的 UI 和静态不变的 UI 放到不同的 Canvas 中。这样,当动态 UI 发生变化时,只触发动态 Canvas 的 Rebatch,静态 Canvas 不受影响。
为什么有效 :UGUI 的网格更新是以 Canvas 为单位的。动静分离的本质是通过隔离 Canvas 层级,将顶点/颜色/材质变更的影响范围限制在动态区域,避免静态元素被连带重绘。
权衡:不同 Canvas 的 UI 元素无法合批,会增加 DrawCall。因此需要在 Rebatch 成本 和 DrawCall 数量 之间做权衡。
实践建议:
- 将血条、技能 CD、飘字、滚动列表等频繁更新的元素放在独立的动态 Canvas 中
- 将背景、静态按钮、装饰性元素等放在静态 Canvas 中
- 不要创建过多的 Canvas------太多 Canvas 本身也会带来额外消耗
4.2 减少节点层次和数量节点数量
直接影响 Rebatch 算法的耗时。减少不必要的 UI 节点,可以有效降低合批计算量。
实践建议:
- 删除不必要的空节点
- 合并可以合并的 UI 元素
- 避免过深的层级嵌套
4.3 优化 Hierarchy 顺序与深度
Rebatch 的合批算法对 UI 元素的深度和重叠情况非常敏感。
实践建议:
- 将所有可能打断合批的层移到最下边的图层
- 尽量避免 UI 元素出现重叠区域
- 使用相同材质贴图的 UI 尽量保持深度相同,这样对合批算法更友好
关于打断合批:当按照 Hierarchy 组织的渲染数据遇到材质、贴图等渲染状态变化时,合批逻辑将会被打断。不同材质的 UI 元素如果相邻,会阻止相同材质的元素合批。
4.4 优化材质与图集
合批的前提是:相同材质 + 相同纹理。一旦材质或纹理不一致,合批就会被中断。
实践建议:
- 合理打包图集:同一个 UI 界面的图片尽可能放到一个图集中
- 统一材质:尽量使用相同的材质和 Shader
- 注意图集大小:不要超过 4096×4096
4.5 关于 Color 修改的注意事项
修改 Image 的 Color 属性,原理是修改顶点色,材质不变,不会产生额外 DrawCall。但会触发网格 Rebatch,同时触发 Canvas.SendWillRenderCanvases。
如果希望改变颜色而不触发 Rebatch,可以考虑直接修改 Shader 的颜色属性------材质不变,没有 Rebatch。
Rebatch 优化本质上是在 DrawCall 和计算开销之间做权衡的艺术。没有放之四海而皆准的优化方案,需要结合具体项目的 UI 复杂度和动态频率,找到最优的平衡点。
二、 Rebuild
1、什么是 Rebuild?
1.1 基本概念
Unity 将 UI 的渲染分为两个步骤:对 Mesh 的操作称为 Rebatch(C++ 层),对 Material 和 Layout 的操作称为 Rebuild(C# 层)。Rebuild 发生在 C# 层面,是指 UGUI 中 Layout 组件调整 RectTransform 尺寸、Graphic 组件更新 Mesh 和 Material,以及 Mask 执行 Cull 的过程。
简单来说,Rebuild 就是 UI 数据的重新计算------当 UI 元素的布局或显示内容发生变化时,UGUI 需要重新计算这些元素的尺寸、位置、顶点数据,并将更新后的数据上传给渲染管线。
1.2 Rebuild 的两个阶段
Canvas 的重建过程可以分为两个主要阶段:
- Layout Rebuild(布局重建)当任何 UI 元素的布局属性发生变化时,UGUI 需要重新计算所有受影响的 UI 元素的最终位置和大小。这个过程涉及遍历 UI 树、计算排版、更新 RectTransform 的内部数据等。
- Mesh Rebuild(网格重建)在布局计算完成后,如果 UI 元素的显示内容发生变化,UGUI 会重新生成这些 UI 元素的渲染 Mesh。这包括创建新的顶点、UV、颜色数据,并将其上传到 GPU。
关键点:布局重建优先于网格重建执行。父容器的尺寸必须先在子元素计算相对位置之前确定,因此 UGUI 的重建是一个精心设计的多阶段管线。
2、Rebuild 的触发条件
理解哪些操作会触发 Rebuild,是优化的第一步。UGUI 通过设置"脏数据"(Dirty)来标记需要重建的元素:
- SetLayoutDirty:标记布局需要重建,加入布局重建队列
- SetVerticesDirty:标记顶点需要重建,加入图形重建队列
- SetMaterialDirty:标记材质需要重建,加入图形重建队列
2.1 触发 Layout Rebuild 的操作
| 操作类型 | 具体示例 |
|---|---|
| RectTransform 修改 | 改变 position、scale、rotation、sizeDelta、anchoredPosition、anchorMin、anchorMax、pivot 等 |
| 父级激活状态改变 | 启用/禁用 RectTransform 的父级 GameObject |
| Layout Group 子元素变化 | 在 Layout Group 中添加、移除、激活或禁用子元素 |
| Layout Group 参数修改 | 修改 spacing、padding、childAlignment 等布局参数 |
| Layout Element 变化 | 修改子元素的 min/preferred/flexible width/height |
注意:只有 LayoutGroup 的直接子节点,并且是 Graphic 类型(如 Image 和 Text)的才会触发 SetLayoutDirty。
2.2 触发 Mesh Rebuild 的操作
| 操作类型 | 具体示例 |
|---|---|
| Text 组件修改修改 | text、fontSize、fontStyle、color、alignment、lineSpacing 等 |
| Image 组件修改 | 修改 sprite、type(Simple/Sliced/Filled)、color 等 |
| RawImage 组件修改 | 修改 texture 属性 |
| Canvas Group 修改 | 修改 alpha 属性 |
3、Rebuild 的性能特点
3.1 主线程执行(最关键的特性)
与 Rebatch 不同------Rebatch 在 Unity 5.2 之后已经优化为多线程执行------Rebuild 完全在主线程执行。
这意味着 Rebuild 的耗时直接占用帧时间,对游戏流畅度的影响更为显著和直接。如果在 Profiler 中看到 Canvas.SendWillRenderCanvases 耗时过高,通常就是 Rebuild 在作祟。
3.2 线性相关
Rebuild 的耗时与发生变化的节点数量基本呈线性相关。也就是说,变化的 UI 元素越多,Rebuild 的耗时越长。
3.3 连带效应
Rebuild 会触发 Rebatch。当一个 UI 元素发生 Rebuild 时,其所在的 Canvas 会被标记为 Dirty,进而触发一次 Rebatch。这意味着 Rebuild 的代价不仅是自身的计算开销,还会连带引发合批计算。
3.4 一个 Canvas,全部重算
在一个 Canvas 上,如果任何一个子 UI 元素被标记为"脏"(Dirty),整个 Canvas 的 Mesh 都可能被强制性地重新计算并上传。这是 UGUI 性能问题中最容易被忽视的"杀手"。
4、Rebuild 的优化策略
针对 Rebuild 的优化,核心思路是:减少触发 Rebuild 的频率,缩小 Rebuild 的影响范围。
4.1 动静分离(最核心的手段)
原理:将频繁变动的 UI 和静态不变的 UI 放到不同的 Canvas 中。这样,当动态 UI 发生变化时,只触发动态 Canvas 的 Rebuild,静态 Canvas 不受影响。
为什么有效:UGUI 的重建是以 Canvas 为单位的。动静分离的本质是通过隔离 Canvas,将顶点/颜色/材质变更的影响范围限制在动态区域,避免静态元素被连带重建。
实践建议:
- 将血条、技能 CD、飘字、滚动列表等频繁更新的元素放在独立的动态 Canvas 中
- 将背景、静态按钮、装饰性元素等放在静态 Canvas 中
- 根据更新频率来分组 UI 元素
4.2 减少 Layout 组件的使用
Layout 组件(如 VerticalLayoutGroup、HorizontalLayoutGroup、GridLayoutGroup)在调整布局时会触发 Layout Rebuild。
实践建议:
- 尽量使用 RectTransform 手动布局替代 Layout 组件
- 如果必须使用 Layout 组件,注意控制其子节点数量
- 避免在运行时频繁修改 Layout 组件的参数
4.3 优化文本更新
Text 属性改变(如倒计时从 "123" 变为 "1234")会触发 SetLayoutDirty。
实践建议:
- 倒计时等定时更新场景,按每秒改变一次即可,不要每帧实时更新
- 使用 TextMeshPro 替代原生 Text,在性能和表现上都有优势
4.4 优化颜色修改
修改 Image 的 Color 属性,原理是修改顶点色,会触发 SetVerticesDirty(顶点改变) 。
实践建议:
- 如果希望改变颜色而不触发 Rebuild,可以考虑直接修改材质球
- 颜色材质不变的情况下修改 Shader 颜色,不会触发 Rebuild
4.5 优化 Image
格式不同的 Image Type 产生的顶点数量不同,直接影响 Mesh Rebuild 的耗时:
| Image Type | 顶点数量 |
|---|---|
| Simple | 4 个顶点 |
| Sliced(勾选 FillCenter) | 36 个顶点 |
| Sliced(不勾选 FillCenter) | 32 个顶点 |
| Tiled | 取决于 RectTransform 大小和原图大小 |
实践建议:在满足视觉效果的前提下,尽量选择顶点数更少的 Image Type。
4.6 优化 Text 特效
Text 的描边(Outline)和阴影(Shadow)会显著增加顶点数量:
- 一个字符产生 4 个顶点
- 加上 Shadow:相当于复制一遍,产生 8 个顶点
- 加上 Outline:复制 4 遍,产生 20 个顶点
实践建议:用相应的 Shader 替代 C# 层面的 Outline 和 Shadow 效果。
4.7 显示与隐藏的优化
控制 UI 显示/隐藏时,SetActive(true/false) 会触发 OnEnable/OnDisable,进而触发 Rebuild。
替代方案:
- 设置 Canvas Group 的 alpha 为 0
- 设置 scale 为 0
- 使用 canvasRenderer.cull = true
这样 VBO(顶点缓冲区)不会被清除,避免了重建的开销。
4.8 关闭不必要的 RaycastTarget
大部分 Image、Text 不参与事件响应,关闭 RaycastTarget 可以减少 EventSystem 的遍历开销。
实践建议:添加工具,在代码中默认取消 Image 和 Text 的 RaycastTarget。
4.9 用 RectMask2D 替代 Mask
Mask 会强制新建 Canvas 并禁用批处理,而 RectMask2D 本身不产生 DrawCall。
记住:Rebuild 的耗时与变化节点数量线性相关。优化 Rebuild 的关键,就是让变化尽可能少、影响范围尽可能小。没有放之四海而皆准的方案,需要结合具体项目的 UI 复杂度和动态频率,找到最优的平衡点。
