这篇笔记最早写于 2024.03,当时是从 Unity 分享课和求职面试的角度整理的。
2026.09 回看时,很多理解已经不一样了。
所以保留 2403 的原始认知作为主体,在每一段下面用 2609 批注 的方式补充、修正、优化。阅读方式:先看正文(2403 的认知),再看下面的
2609 批注。
一、合批处理在 UGUI 中会有一些不同
2403 原文
先获得一个按 Hierarchy 顺序的列表,计算每个物体的深度。
- 深度从
0开始递增。如果世界包围盒 Z 轴不为0(或isCanvasInjectionIndex),则需独占一个批次,同时独占一个深度。即等于之前所有物体最大深度+1,后一个物体深度需要+1。 - 对一般物体的深度,会判断是否可跟之前的物件共享深度,走接下来的流程。
- 2.1 先按格子(默认大小是
120,根据包围盒再计算)划分出多个格子。(只是为了加速求交) - 2.2 计算物体包围相交哪些格子,再跟格子中已有的物体进行包围盒相交判断。
- 如果不相交则使用当前深度;
- 如果相交且可合批,则使用相交物体中最大的深度;
- 如果相交且不可合批,则使用相交物体中最大的深度
+1。 - 合批条件:无独占批次 + 材质相同 + 贴图相同 + 裁剪开关和裁剪矩形相同 + 贴图 A8 格式一致(
kTexFormatAlpha8)。
- 2.3 将该物体加入所有相交的格子中。若遇到独占深度的物体,则格子数据清空。即后续物件不跟之前的物件共享深度。
- 2.1 先按格子(默认大小是
排序:按照 深度 → 材质 → 贴图 → 层级顺序 优先级排序。
合批:对排序后的列表,从头开始一个一个检测是否能与前面的物体合批。合批条件:无独占批次(只判断 isCanvasInjectionIndex)+ 材质相同 + 贴图相同 + 裁剪开关和裁剪矩形相同 + 贴图 A8 格式一致(kTexFormatAlpha8),非 SubBatch 只判断前两个条件,一般情况下 UI 的材质都一样。
在 Unity 的 Shader 中会将这些数据存到一些通道里:POSITION、NORMAL、COLOR、TANGENT、TEXCOORD。
UGUI 批处理原文链接:
https://blog.csdn.net/hankangwen/article/details/122667647
2609 批注
这段描述的是 UGUI 内部如何决定哪些 Graphic 可以共享同一个 depth / batch。当时是把它当成一套“流程”记下来的,现在回看,更重要的是抓住它的本质:
1. 合批的顺序是“先排序,再贪心合批”,不是先合批再排序。
这一点 2403 时其实没完全吃透。当时容易误以为 UGUI 会主动去找能合的物体合在一起,实际上它是:
- 先按
Hierarchy顺序收集 Graphic; - 根据包围盒相交关系算出每个物体的 depth;
- 按
depth → 材质 → 贴图 → 层级排序; - 然后从头到尾贪心地检测“这个能不能和前面那个合”。
贪心的意思是:一旦断了,后面即使条件满足也回不去。所以“中间插一个不同图集的元素”代价很大——它会把前后彻底切成两个批次。
2. 深度计算的核心是“包围盒相交”,格子只是为了加速。
2403 时把“格子大小 120”当成一个知识点记下来了,但现在看,格子大小本身不重要,它只是空间划分加速的手段,类似四叉树/网格。真正决定 depth 的是:
- 两个物体的包围盒是否相交;
- 相交的话,能不能合批。
不相交 → 可以共享深度;相交且可合批 → 取相交物体最大深度;相交且不可合批 → 最大深度 +1。
3. 合批条件里,工程上真正重要的是前几个。
2403 把 kTexFormatAlpha8、SubBatch 这些都记下来了,但实际项目中,最常打断合批的只有:
- 不同图集
- 不同材质
- Mask / RectMask2D
- 中间插入了不一样的元素
kTexFormatAlpha8 属于底层细节,日常优化优先级很低。SubBatch 更是普通 UI 开发几乎不用关心的概念。
4. Shader 通道那部分,2403 只是“知道”,2609 要知道“为什么”。
UGUI 会把顶点数据打包进 POSITION、NORMAL、COLOR、TANGENT、TEXCOORD 这些通道。为什么?因为 UGUI 用的是一套通用顶点格式,为了塞下 UV、顶点色、裁剪矩形等额外数据,会复用一些通道。比如:
TEXCOORD0存 UV;COLOR存顶点色;TANGENT/NORMAL有时被复用来存裁剪矩形等额外信息。
这些在写自定义 UGUI Shader 时才会真正用到,日常优化不必深究。
一句话修正:
2403 记的是“流程和条件”,2609 要抓的是“排序 + 贪心合批”这个本质,以及“哪些条件在工程上真正重要”。
二、UGUI 合批的注意事项
2403 原文
首先合批的原理:同一 Canvas 下,同一材质,同一图片或者是图集。
UGUI 的排序
首先 UGUI 的排序渲染方式:
- Canvas 的渲染层级
- UI 的深度渲染层级
- index 深度
- 材质的渲染层级
- Hierarchy 次序(textureID 排序)
UGUI 都是先进行排序,后进行合批。
排序的顺序取决于 index 深度(box 是否遮盖的关系)、材质 ID、textureID 排序。
建议
- 所有有可能打断合批的层移动到最下边图层,避免 UI 元素重叠。
- 拆分多个同级或者嵌套的 Canvas,减少 Canvas 的 rebatch 复杂度。
- 动静分离。
- Canvas 尽量使用
Overlay模式减少 Camera 的性能开销。
2609 批注
1. “同一 Canvas + 同一材质 + 同一图片/图集”只是表象。
本质是:同一 Batch 内不能切换渲染状态。材质、贴图、裁剪状态、Stencil 状态任意一个变了,就得新 DrawCall。所以“合批”不是 UGUI 主动去合并,而是尽量不让渲染状态切换,让它们自然落入同一个 Batch。
2. 排序那几条,2403 是背下来的,2609 要知道开发者能影响什么。
- Canvas 的渲染层级 →
Sort Order、Render Mode、Sorting Layer; - UI 的深度渲染层级 → 由包围盒相交算出的 depth;
- index 深度 →
Hierarchy中的先后顺序; - 材质的渲染层级 → 材质 ID 参与排序;
- Hierarchy 次序 → 同深度同材质下按 textureID 排序。
开发者真正能影响的是:层级、父子关系、图集划分。其他都是 UGUI 内部行为。
3. 四条建议,每一条都能说出“为什么”和“代价”。
| 建议 | 2403 的理解 | 2609 的修正 |
|---|---|---|
| 打断合批的层移到最下边 | 避免 UI 元素重叠 | 合批是贪心的,中间插一个不同图集就会断。移到最下面只是减少打断概率,不是根治 |
| 拆分多个 Canvas | 减少 rebatch 复杂度 | 一个 Canvas 脏了会触发整个 Canvas 的 Rebuild + Rebatch。拆分是为了缩小脏区域,代价是 Canvas 数量增加,可能增加 DrawCall |
| 动静分离 | 优化手段 | 动态元素频繁触发 Graphic Rebuild,静态元素不该陪跑。但要注意:同 Canvas 下动静分离才有意义,拆 Canvas 本身也有开销 |
| Canvas 用 Overlay | 减少 Camera 开销 | Overlay 省掉 Camera 相关开销,但代价是不能和 3D 内容穿插,是权衡不是绝对 |
4. 2403 没提到、2609 要补的两条:
RectMask2D比Mask更轻:Mask会引入额外材质 + Stencil 状态,直接打断合批;RectMask2D只做矩形裁剪,不产生额外 DrawCall。能用RectMask2D就别用Mask。SetActive代价大:它同时触发 Layout + Graphic Rebuild,还会标记 Canvas 脏。能改CanvasGroup.alpha或移出屏幕,就别频繁SetActive。
一句话修正:
2403 记的是“建议”,2609 要知道每条建议对应的底层规则、代价、以及适用边界。
三、UGUI 核心底层原理
2403 原文
说点特别的,为什么写这个:
- 上面的合批是因为在 Unity 分享课上面进行看到的。
- 为什么写这个,是因为最近求职,因为自己是中高级的,之前都是业务,拼 UI,所以偶尔碰到资深面试官,都会进行询问对应 Unity UGUI 的一些东西,所以由此而来。
首先我就说说我对这个的理解吧:
- UGUI 渲染是
Hierarchy从上到下从左到右遍历,每个Canvas独立收集所有Graphic组件,根据材质和图集贪心合批,生成DrawCall。 - 分为两个阶段:一个是 Build(构建),一个是 Batch(CPU → GPU 的过程)。
- Rebuild 分两个阶段:Layout 阶段是有布局组件时重新计算位置大小;Graphic 阶段是顶点数据变化时重新生成 Mesh。
- Rebatch 是合批被打断 —— Canvas 下从上到下贪心合批,中间如果有不同图集或不同材质的 Image,就会打断合批,产生新的
DrawCall。
| 阶段 | 做什么 | 什么时候触发 |
|---|---|---|
| Layout 阶段 | 重新计算布局(位置 / 大小) | 有布局组件(LayoutGroup / ContentSizeFitter)时才触发 |
| Graphic 阶段 | 重新生成 Mesh(顶点数据) | Image 改了 Sprite / 颜色 / 位置时触发 |
所以所谓的什么 DC、什么动静分离,这种其实就是从原理规范所衍生出来的规范,避免底层规则触发。
Rebatch 是合批被打断 —— 不同图集、不同材质、Mask 都会打断合批。优化手段就是功能区分图集、图集打包、动静分离。
什么打断合批
- 不同图集
- 不同材质
- Mask / RectMask2D
- 中间插了一个不一样的元素
什么会造成 Rebuild
SetActive 为什么触发 Rebuild?
SetActive(false) 会触发整个子树 OnDisable,SetActive(true) 触发 OnEnable,同时标记 Canvas 脏数据,触发完整 Rebuild(Layout + Graphic 都重新算)。
综上所述,其实就好比很多时候的东西,其实应该都是先读懂底层,才知道架构在上面的方法为什么可以实现优化,优化了什么东西。
2609 批注
1. “Rebuild ≠ Rebatch” 是这条分界线,2403 摸到了,2609 要划清。
- Rebuild 是“数据变了,要重新算”:
- Layout Rebuild:布局变了(位置 / 大小);
- Graphic Rebuild:顶点变了(Mesh 重生成)。
- Rebatch 是“合批断了,要重新组 Batch”:
- 即使顶点没变,只要排序结果变了,也可能触发 Rebatch。
所以“改个颜色”可能只触发 Graphic Rebuild,而“插入一个不同图集的元素”会直接打断合批。两者不是一回事。
2. 触发表格,2609 要补细节。
| 阶段 | 做什么 | 什么时候触发 | 2609 补充 |
|---|---|---|---|
| Layout 阶段 | 重新计算布局 | 有布局组件时 | 嵌套布局组件是重灾区,会连锁触发子节点重建 |
| Graphic 阶段 | 重新生成 Mesh | Image 改 Sprite / 颜色 / 位置 | 改颜色只触发 Graphic Rebuild,不一定打断合批 |
| Rebatch | 重新组 Batch | 排序结果变化、渲染状态切换 | 即使顶点没变,插入不同图集元素也会触发 |
3. 打断合批的原因,2403 列了现象,2609 要说清机制。
| 原因 | 2403 | 2609 |
|---|---|---|
| 不同图集 | 知道会打断 | 渲染状态切换,必须新 DrawCall |
| 不同材质 | 知道会打断 | Shader / 参数不同,必须新 DrawCall |
| Mask / RectMask2D | 知道会打断 | Mask 引入额外材质 + Stencil,RectMask2D 只做矩形裁剪更轻 |
| 中间插入不同元素 | 知道会打断 | 合批是贪心的,断了后面回不去 |
| 独占批次物体 | 知道会独占深度 | 如 RenderTexture、自定义材质、嵌套 Canvas |
4. SetActive 的代价,2403 只说了一半。
2403 说它触发完整 Rebuild(Layout + Graphic)。2609 要补:
- 它还会标记 Canvas 脏,触发 Rebatch;
OnDisable/OnEnable本身还会触发用户脚本逻辑;- 频繁
SetActive是 UI 卡顿的常见原因。
替代方案:改 CanvasGroup.alpha、移出屏幕、改 localScale 等,视具体需求而定。
5. 2403 说“先读懂底层”,2609 要加一句“读底层是为了推导规则”。
比如:
- 为什么
Mask会打断合批?→ 因为它引入了额外的材质 / Stencil 状态; - 为什么
RectMask2D比Mask更轻?→ 因为它不产生额外 DrawCall,只做矩形裁剪; - 为什么
SetActive代价大?→ 因为它同时触发 Layout + Graphic Rebuild,还会标记 Canvas 脏。
这些都不是“口诀”,而是可以从原理推出来的结论。
一句话修正:
2403 知道 Rebuild 和 Rebatch 是两回事,2609 要知道它们各自的触发条件、代价、以及如何避免。
四、认知升级总结
2403 的总结
先读懂底层,才知道架构在上面的方法为什么可以实现优化、优化了什么东西。
2609 的修正
2403 的总结是对的,但还停留在“知道要读底层”的阶段。现在会再加一句:
读底层不是为了背规则,而是为了在遇到新问题时,能自己推导出规则。
具体到 UGUI 合批,认知升级体现在:
| 维度 | 2403 | 2609 |
|---|---|---|
| 出发点 | 从规则出发 | 从机制出发 |
| 关注点 | 知道有这个规则,会用它 | 知道它为什么这样,还能推出其他用法 |
| 优化方式 | 背建议:合图集、动静分离、拆 Canvas | 先问瓶颈是 Rebuild 还是 Rebatch,再决定手段 |
| 遇到新问题 | 查资料、问人 | 能从原理推导出结论 |
| 对规则的态度 | 规则说不能这样,那就别这样 | 规则背后的约束是什么,能不能绕过或换一种方式 |
| 对代价的认知 | 知道有代价 | 知道代价多大、在什么条件下可接受 |
可迁移的追问
2403 时不会问、2609 时会问的问题:
- 为什么
Mask会打断合批?→ 引入额外材质 / Stencil 状态。 - 为什么
RectMask2D比Mask更轻?→ 不产生额外 DrawCall,只做矩形裁剪。 - 为什么
SetActive代价大?→ 同时触发 Layout + Graphic Rebuild,还会标记 Canvas 脏。 - Canvas 的
willRenderCanvases在哪一步调用?→ 由CanvasUpdateRegistry调度。 CanvasRenderer和CanvasBatch什么关系?→CanvasRenderer是数据载体,CanvasBatch是合批结果。
一句话:
2403 是“知道”,2609 是“理解 + 迁移”。
参考链接