拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Unity UGUI 性能优化实战:TX 工作室 DrawCall 分析与 UI 重建机制深度剖析

示例工程【免费下载链接】Unity3DTraining【Unity杂货铺】unity大杂烩~项目地址https://gitcode.com/gh_mirrors/un/Unity3DTraining点击查看免费下载本文基于《TX工作室UI优化文档》整理成篇。该文档是腾讯系工作室在 UGUI 项目中的实战经验沉淀聚焦于 UGUI 的 DrawCall下称 DC产生条件、SendWillRenderCanvases重绘时机、UI Despawn 方案选型、以及无 Canvas / 有 Mask / 有 Canvas 三种场景下的 DC 计算规则并给出了可直接落地的六条 UI 规范。读完本文你将能够准确判断一次 UI 操作是否会触发 Canvas Rebuild能在编辑器与真机上分析 DC 构成并掌握使用CanvasRenderer.color、CanvasGroup.alpha、EmptyGraphic 等技巧在不牺牲表现的前提下压低 DC 与 CPU 消耗。一、UGUI DrawCall 的产生条件在动手优化之前必须先搞清楚 UGUI 到底在什么情况下会产生 DrawCall。文档给出了三条关键规则区域交汇才计算当 Canvas 下所有节点区域的最小 AABB 包围盒与 Canvas 的绘制区域有交汇时才会进行 DrawCall 计算。即便发生了 DrawCall 计算Canvas 自身也固定有2 个 DrawCall 消耗。移出屏幕仍然计算当 Canvas 进行 DrawCall 计算时即便把子元素移出至屏幕外移出的子元素依然参与 DrawCall 计算。移出屏幕的重叠依然会增加 DC移出屏幕外的子元素其 DrawCall 计算规则与仍在屏幕内的元素完全一致。因此如果移出屏幕外的元素有区域重叠同样会使 DrawCall 增高。这三点是后续所有优化手段的底层依据“移出屏幕”并不能让 UI 免于 DC 计算这也是文档在 Despawn 方案对比中反复强调移出屏幕外同样会进行 DrawCall 计算的原因。二、UI 重绘时机SendWillRenderCanvasesUGUI 的重绘由SendWillRenderCanvases触发而触发它的前置条件是ModifyMesh。文档总结的触发规则如下尺寸/锚点/轴心变化触发RectTransform 修改了 SizeDelta、Anchor、pivot 都会触发ModifyMesh进而触发SendWillRenderCanvases进行 UI 重绘而其他的 SQTScale / Rotation / Translation变化并不会触发ModifyMesh。切换 Active 触发切换 UI 的 Active 状态之后会触发ModifyMesh带来SendWillRenderCanvases消耗。alpha 不触发切换 UI 的 CanvasGroup 或 CanvasRenderer 的 alpha不会触发ModifyMesh。也就是说设置 alpha 为 0 能够达到与SetActive(false)相同的“看不见”效果并且 alpha 设置为 0 之后也不会再进行 DrawCall 计算。切换 Parent 触发切换 UI 的 Parent 之后也会触发ModifyMesh引发SendWillRenderCanvases消耗。这条规则直接催生了文档中用 alpha 代替 SetActive的核心优化思路既然 alpha 为 0 既不触发重绘也不产生 DC那么在需要隐藏 UI 时它就比SetActive(false)更省。三、UI Despawn 的四种方式对比文档将 UI 隐藏Despawn的常见做法归纳为四种并逐一分析了优缺点方式优点缺点1.SetActive(false)最直接无 bug会造成SendWillRenderCanvases2. 将 UI 节点移出屏幕外不会产生SendWillRenderCanvases移出屏幕外同样会进行 DrawCall 计算需要进行 UI 区域判断不能造成 UI 重叠增加 DC需要手动关闭 Update 函数需要注意 OnEnable / OnDisable 中的逻辑是否能正常工作如果 Parent 中有 Layout需要在移出屏幕时给 UI 添加 IgnoreLayout3. 给 UI 添加 CanvasGroupDespawn 时设置 alpha 为 0不会造成SendWillRenderCanvases不会进行 DrawCall 计算如果 Parent 包含 Layout 仍需添加 IgnoreLayout需要手动关闭 Update 函数需要注意 OnEnable / OnDisable 逻辑需要注意 CanvasGroup 参数造成的其他逻辑因素4. 切换 Parent无会造成SendWillRenderCanvases切换 Parent 会造成其他消耗比如重新设置 SQT 等变量综合来看方案 3CanvasGroup alpha0在不触发重绘、不产生 DC两个维度上表现最佳但引入的额外成本是必须手动管理 Update 逻辑与 Layout 忽略标记。值得注意的是方案 2 与方案 3 都强调需要手动关闭 Update 函数——因为隐藏的 UI 若仍在运行 Update省下的重绘开销会被逻辑开销抵消。在仓库中可以看到实际项目对方案 1 的使用背包系统示例中TooltipsUI.cs 的Show()/Hide()直接调用gameObject.SetActive(true/false)DragItemUI.cs 同样如此。这正是文档建议优化的典型场景频繁弹窗的 Tooltips 若改用 CanvasGroup.alpha 控制显隐可避免每次弹出造成的 Canvas Rebuild。四、关键实测数据Color 与 Active 的 CPU 开销文档给出了一组基于66 个 Image的实测对比数据是选择 API 时的重要参考1. 修改 Image.color 与修改 CanvasRenderer.color 的对比修改 Image.colorCPU 消耗约15msGPU 消耗约0.7ms修改 CanvasRenderer.colorCPU 消耗约1.3msGPU 消耗约0.7ms结论如果修改CanvasRenderer.color能够达到与修改Image.color一样的效果优先修改CanvasRenderer。原因是Image.color与Text.color都会造成 Canvas Rebuild而修改CanvasRenderer.color则不会。2. 切换 UI 的 Active 与切换 CanvasGroup 的对比设置 ActiveCPU 消耗20ms设置 CanvasGroupCPU 消耗1.3ms同样是 66 个 Image 的测试规模SetActive的 CPU 开销是 CanvasGroup 的 15 倍以上进一步印证了上一节的方案选型结论。3. 重复设置的惰性行为文档还验证了重复设置不引起 Rebuild的惰性行为这意味着一味做值是否变化的判断并不能省下所有开销但反过来也说明相同值的重复赋值是安全的重复设置 Image 的 color相同 Color不会引起 Rebuild重复设置 Text 的 text相同 text不会引起 Rebuild重复设置 Image 的 sprite相同 Sprite不会引起 Rebuild重复设置 Image 的 fillAmount相同数值相差小于 0.000001f不会引起 Rebuild五、UGUI DrawCall 分析过程文档强调UGUI 的 DrawCall 分析过程应分三种情况讨论不能一概而论。1. 无 Canvas、无 Mask 情况最简单分析过程简述如下首先判断当前 UI 所占的 Rect 区域相对于整个 Root 节点底下是否有重叠如果没有重叠当前就是第 0 层将当前层级记录下来。深度优先迭代 Transform 下的所有孩子从最顶层的层级开始判断查看当前 UI Rect 所占区域是否与该层级的任意 UI 节点重叠。如果重叠判断当前 UI 节点是否能被该层级的 UI 节点 Batch能则归入该层级否则归入后面一个层级如果没有重叠则依次向前面的层级检测最低层级为 0。得到所有 UI 节点及对应层级后将每个层级中的 UI 节点进行 Batch 合并分析能够合并的放入同一个 Batch 结构。对所有 Batch 结构排序。可确认的排序规则是Text 的 Batch 先绘制之后才是 Image 的绘制有 Mask 时Mask 的 Text 与 Mask 的 Image 会比无 Mask 的后绘制。需要注意的是即便是 Text由于 font 不同或 Text 材质不同也不能进行 Batch。此时 Batch 排序如何判断文档作者猜测是根据 Hierarchy 中的先后顺序得出的Image 也不例外。最后将所有层级从下到上取出所有 Batch 集合放进统一集合迭代该集合如果相邻的两个 Batch 之间能够再进行合并就合并为一个 Batch。最终得出的 Batch 集合就是 UI 的 DC 数目也就是最后 UI 的渲染顺序。2. 无 Canvas 但有 Mask 情况出现 Mask 后情况复杂得多差异点如下如果当前节点上存在 Mask 组件该节点下的所有子孩子都会添加 Mask 标签。由于 Mask 下面还能再出现 MaskMask 标签应该用数量记录相同层级的相同数量 Mask 能够合并但直属 Mask 节点的合并规则与孩子节点的合并规则不同。每一个 UI 节点上的 Mask 数量不同肯定不能进行合并。同一个层级进行 Batch 排序时Mask 标签数量越大排序越靠前。每添加一个 Mask 组件会多出一个 DC。用 Unity 5.x 的 FrameDebug 观察这个 DC 并没有绘制内容只是在做 UI 的 AlphaClip。该 DC 也可以被合并如果出现时机一致即 MaskA 所占的 ImageA 能与 MaskB 所占的 ImageB 合并那么这个 Alpha 裁剪 DC 也能被合并。MaskA 所占 ImageA 与 MaskB 所占 ImageB 能合并的条件是ImageA 与 ImageB 本身能够合并并且 ImageA 与 ImageB 没有发生重叠。直属 Mask 的节点不能与节点下的子节点合并所有子节点的层级都相对 1。如果 MaskB 与 MaskA 发生了重叠那么 MaskB 的层级比 MaskA 子节点下层级最高的多 1因此会出现当 MaskA 的节点全部渲染完毕、并且出现了 MaskA 的 AlphaClip DC 之后才可能进行 MaskB 的渲染。如果当前 Mask 的子节点都出现在 Mask 外面即便看不见了仍会被计算 DrawCall同时会先渲染出现在 Mask 外面的节点如果 Mask 的所有子节点都出现在 Mask 外面Mask 自身的渲染会被放在最后面——这并不会对 DC 数量有所改变。如果 Mask 外的当前节点与 Mask 直属节点的 Rect 重叠当前节点的层级为 Mask 最大子节点层级 1如果只是与 Mask 的子节点区域重叠则只是 Mask 子节点的层级 1。Mask 的子节点如果原本可以 Batch但其中一个在 Mask Rect 外部、一个在 Mask Rect 内部那么它们不能一起 Batch。在 OutOfMask 情况下如果 Mask 的子节点所处 Rect 的重叠区域只有 Mask 本身该子节点的层级与 Mask 节点层级一致文档标注为诡异如果重叠区域是 Mask 下的其他子节点层级为重叠子节点层数 1。3. 有 Canvas 情况Canvas 的存在让规则变得非常清晰UGUI 的合并策略是以 Canvas 为单位的上述所有分析过程都发生在单个 Canvas 之下。如果在 Hierarchy 中出现 Canvas会直接打断当前 Hierarchy 的合并策略。例如在一个 Canvas 的孩子节点中间出现一个子 Canvas就可以直接按三个 Canvas 的层级进行处理。六、DC 计算的三个例子例 1无 Canvas、无 Mask一个 ImageA 层级为 0一个 ImageB 层级为 1一个 Text 层级为 0最后 DC 为2。渲染顺序为 Text → ImageA → ImageB因为相邻的 batch 能继续合并所以 ImageA 与 ImageB 最终合并。一个 TextA 层级为 0一个 ImageA 层级为 0一个 TextB 层级为 1。按分析过程 DC 应为 3TextA → ImageA → TextB但实测诡异地为2渲染过程为 ImageA → TextA TextB。作者据此猜测Unity 的 Batch 过程可能对 batch 排序做了简单调整。一个 TextA 层级为 0一个 ImageA 层级为 0一个 ImageB 层级为 0一个 TextB 层级为 1。按上述实验推测 DC 应为 2ImageA ImageB → TextA TextB但实际 DC 为3过程为 TextA → ImageA ImageB → TextB。可见例 2 只能算意外。不过可以通过一些手段使 TextA 与 TextB 能够 Batch最终 DC 也能降到 2具体做法在第七节优化规范中说明。例 2无 Canvas 但有 MaskImageA-Mask 层级为 0ImageB 层级为 2ImageC 层级为 2TextA 层级为 2。如果 ImageA 不存在 MaskDC 应为 2ImageA 与 ImageB、ImageC 可合并加上 Mask 后 DC 变为4过程为 ImageA-Mask → TextA → ImageB ImageC → AlphaClip。两个互不重叠的 Mask 场景ImageA1-Mask 层级为 0ImageA2 层级为 2ImageA3 层级为 2TextA 层级为 2ImageB1-Mask 层级也为 0两个 Mask 未重叠ImageB2 层级为 2ImageB3 层级为 2TextB 层级为 2。DC 仍为4过程为 ImageA1-Mask ImageB1-Mask → TextA TextB → ImageA2 ImageA3 ImageB2 ImageB3 → AlphaClip。如果 MaskB 与 MaskA 发生了重叠则 ImageB1-Mask 层级变为 3因为 MaskA 子节点最高层级为 2ImageB2 层级为 4ImageB3 层级为 4TextB 层级为 4最终 DC 为8过程为 ImageA1-Mask → ImageA2 ImageA3 → TextA → AlphaClip → ImageB1-Mask → ImageB2 ImageB3 → TextB → AlphaClip。例 3有 CanvasHierarchy 中排序为 ImageA、ImageB、ImageC原本 DC 应为 13 个 Image 都可合并。给 ImageB 添加 Canvas 后DC 变为3ImageB 自成一个 DC 的同时把 ImageA 与 ImageC 的相邻关系也打断了。七、六条可落地的 UI 优化规范基于以上原理文档给出了六条明确的项目规范1. 事件触发区域统一使用 EmptyGraphic其他 Graphic 的 Raycast Target 都设为 falseEmptyGraphic 是只有逻辑区域、没有显示区域的 Graphic不产生 DrawCall。Image、Text 等默认 Graphic 都会勾选 Raycast Target表示当前 RectTransform 区域点击有效。但过多的 Raycast Target 会让 EventSystem 产生更多消耗。所有事件触发区域统一使用 EmptyGraphic还利于 UI Despawn 与 Spawn 的统一操作。2. 将 Image.color、Text.color 都改为 CanvasRenderer.color修改 Image.color 与 Text.color 都会造成 Canvas Rebuild但修改 CanvasRenderer.color 不会。大部分颜色需求都能通过调节 CanvasRenderer 的 Color 实现效果。文档作者坦诚修改 CanvasRenderer.color 之后 UI 还能继续 Batch 的原理当时尚未完全弄清但实测效果是喜人的见第四节 15ms vs 1.3ms 的数据。3. 限制 UI GameObject 的 SetActive / SetDeactive 操作每次 UI 更新 Active 标记都会造成 Canvas Rebuild 消耗。使用CanvasRenderer.setAlpha(0)或CanvasGroup.alpha 0的方式同样能达到 UI SetDeactive 的效果且不产生 DC 与 Canvas Rebuild但此时点击区域与 Update 更新需要手动关闭。将 UI 移除出屏幕外也是一种做法但缺陷较大移除出屏幕外的 UI 同样计算 DC如果它们重叠在一起会造成 DC 不可预期增长同样需要手动关闭 Update。避免频繁设置 UI 的 Parent因为每次设置都会造成 Canvas Rebuild。建议通过中间代理UIObjectRoot进行 UI GameObject 的 Deactive 操作。4. 尽量避免使用 Mask使用 Mask 会造成多余 DC几乎每多一个 Mask 就会产生一个 DrawCall尽管 Mask 也能 Batch。Mask 会打破统一的 DC Batch 机制使 DC 数量更难以控制。优先使用RectMask2D它与 Mask 的原理不一样不会产生多余 DC也不会影响 DC 的计算规则当然也有特定限制——只能用于 Rect 区域。5. 不使用 Text 的 Best Fit6. 尽量避免 Unity 提供的 Outline 与 ShadowOutline 与 Shadow 会产生多4 倍的顶点数难以接受。使用自己提供的 SingleOutline只多一倍的顶点数并且更符合美术预期。八、其他实战要点与合批补充文档还记录了若干零散但实用的结论设置位置需判重设置 UGUI 元素位置时就算设置的位置和原位置一样也会导致 Canvas 重建所以设置位置时最好先做相等判断。顶点数据缓存的共性UGUI 和 NGUI 都有一个类来存储顶点、uv、颜色等信息每一次重新绘制或更改都会重新填充其中的数据。静态化高消耗元素Outline 和 Tiled Image、长文本最好不要做成动态的绘制消耗太大Outline 会复制 5 个原网格顶点数和边数增加 5 倍相比标准 Outline 官方组件多 4 倍的顶点这个5 个原网格的表述来自工程实践中的另一个观察口径两条数据都指向同一个结论Outline 对网格的膨胀是致命的。UGUI 合批的本质补充知识点对于每一个 UI 元素对应其材质和 shader 找到一个 batch没有 batch或者有 batch 但被其他 batch 的 UI 挡住了就要新生成一个 batch否则就合到已存在的 batch。关于第三点中通过抬高 Text 层级合并 Text的思路文档在作用一节专门说明工具可以在编辑状态分析当前 UI 的 DC 消耗以指导优化 DC并提供 Text 组件 DC 优化功能——通过抬高 Text 组件的层级达到整体合并 Text 组件的作用。以此类推其他的 batch 也能通过类似的抬高层级操作进行 UI 之间的 Batch 优化。这正是第八节例 1 中使 TextA 与 TextB 能够 Batch最终 DC 降到 2的具体做法。九、在仓库中继续深入这份优化文档是 PerformanceOptimization 专题目录下的一份实战沉淀与该目录中的 AnimOptimization.md、UGUI的优化.docx、ProfilerExample 等互为补充。仓库的 UGUITraining 目录则提供了大量可运行验证的 UGUI 项目KnapsackSystem背包系统其中 TooltipsUI.cs 展示了文档提到的典型频繁 SetActive写法可作为对照优化场景UGUIDemo01 与 UGUIDemo02菜单、技能、背包、任务列表等完整 UI 界面Nightmares_Demo完整的 UGUI 系统应用案例RectTransform.mdRectTransform 相关知识点可与本文修改 SizeDelta/Anchor/pivot 触发 ModifyMesh的规则互相印证。阅读本文后建议在 Unity 编辑器中打开 FrameDebugger 逐帧观察第六节中的三个例子再结合第五节的分析规则即可逐步建立看到 Hierarchy 结构就能估算 DC 数量的能力。赞分享示例工程【免费下载链接】Unity3DTraining【Unity杂货铺】unity大杂烩~项目地址https://gitcode.com/gh_mirrors/un/Unity3DTraining点击查看免费下载相关推荐GhostTrack在终端里查询 IP 归属地和手机号的入门指南GhostTrack在终端里查询 IP 归属地和手机号的入门指南 假设你收到一条陌生来电想知道号码大概属于哪个地区或者在运维日志里看到一堆陌生 IP想快网络安全CLIKotlin/Native性能优化内存管理与GC机制深度剖析Kotlin/Native性能优化内存管理与GC机制深度剖析 内存管理架构概览 Kotlin/Native采用 内存安全模型 与 显式内存管理 相结合的架构编译器语言运行时编程语言Phoenix 实验恢复机制深度剖析原子认领、孤儿扫描与工作重建Phoenix 实验恢复机制深度剖析原子认领、孤儿扫描与工作重建 导读 本篇文章深入解析 PhoenixAI Observability Evaluat可观测性AI 评测LLMOpsAI 应用人工智能上一篇VK视频下载工具3种方法快速保存VK视频到本地离线看下一篇MAA 明日方舟日常助手实操指南6 步跑通一键全日常队列附 8 个常见坑的自救方法创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门