加载中...
UGUI 合批(DrawCall)原理与优化笔记
发表于:2024-03-22 | 分类: 图形渲染

这篇笔记最早写于 2024.03,当时是从 Unity 分享课和求职面试的角度整理的。
2026.09 回看时,很多理解已经不一样了。
所以保留 2403 的原始认知作为主体,在每一段下面用 2609 批注 的方式补充、修正、优化。

阅读方式:先看正文(2403 的认知),再看下面的 2609 批注。


一、合批处理在 UGUI 中会有一些不同

2403 原文

先获得一个按 Hierarchy 顺序的列表,计算每个物体的深度。

  1. 深度从 0 开始递增。如果世界包围盒 Z 轴不为 0(或 isCanvasInjectionIndex),则需独占一个批次,同时独占一个深度。即等于之前所有物体最大深度 +1,后一个物体深度需要 +1。
  2. 对一般物体的深度,会判断是否可跟之前的物件共享深度,走接下来的流程。
    • 2.1 先按格子(默认大小是 120,根据包围盒再计算)划分出多个格子。(只是为了加速求交)
    • 2.2 计算物体包围相交哪些格子,再跟格子中已有的物体进行包围盒相交判断。
      • 如果不相交则使用当前深度;
      • 如果相交且可合批,则使用相交物体中最大的深度;
      • 如果相交且不可合批,则使用相交物体中最大的深度 +1。
      • 合批条件:无独占批次 + 材质相同 + 贴图相同 + 裁剪开关和裁剪矩形相同 + 贴图 A8 格式一致(kTexFormatAlpha8)。
    • 2.3 将该物体加入所有相交的格子中。若遇到独占深度的物体,则格子数据清空。即后续物件不跟之前的物件共享深度。

排序:按照 深度 → 材质 → 贴图 → 层级顺序 优先级排序。

合批:对排序后的列表,从头开始一个一个检测是否能与前面的物体合批。合批条件:无独占批次(只判断 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 排序。

建议

  1. 所有有可能打断合批的层移动到最下边图层,避免 UI 元素重叠。
  2. 拆分多个同级或者嵌套的 Canvas,减少 Canvas 的 rebatch 复杂度。
  3. 动静分离。
  4. 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 原文

说点特别的,为什么写这个:

  1. 上面的合批是因为在 Unity 分享课上面进行看到的。
  2. 为什么写这个,是因为最近求职,因为自己是中高级的,之前都是业务,拼 UI,所以偶尔碰到资深面试官,都会进行询问对应 Unity UGUI 的一些东西,所以由此而来。

首先我就说说我对这个的理解吧:

  1. UGUI 渲染是 Hierarchy 从上到下从左到右遍历,每个 Canvas 独立收集所有 Graphic 组件,根据材质和图集贪心合批,生成 DrawCall。
  2. 分为两个阶段:一个是 Build(构建),一个是 Batch(CPU → GPU 的过程)。
  3. Rebuild 分两个阶段:Layout 阶段是有布局组件时重新计算位置大小;Graphic 阶段是顶点数据变化时重新生成 Mesh。
  4. 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 是“理解 + 迁移”。


参考链接

上一篇:
UitoolKit+ GraphView 开发 可视化节点编译器
下一篇:
关于图形学
本文目录
本文目录