Files
2026-09-10 17:29:24 +08:00

12 KiB
Raw Permalink Blame History

DC 与帧率排查经验

从 T5 / T11 两个工程的实际排查里沉淀下来的方法论与踩坑记录。 配合 01_UIEffect材质共享降DC/移植与验证手册.md 使用:手册负责"怎么改",本文负责"改完怎么看、看不明白怎么办"。 全屏遮挡机制的专项排查(含团结引擎加密 guid 的坑)见 02_全屏面板遮挡省DC/FullScreenCover_使用方法与避坑.md 第八章。


一、Frame Debugger 看图技巧(最常用)

Window → Analysis → Frame Debugger → Enable。

1.1 纹理名怎么读

纹理名形态 含义
sactx-0-1024x1024-DXT5|BC3-atlas-<hash> SpriteAtlas 运行时图集(sactx = Sprite Atlas Texture)。<hash> 标识具体哪个图集,两个批次 hash 不同 = 不同图集 = 不可能合批
Font Texture / xxx SDF TextMeshPro / 字体图集,跟普通 Image 天然不同纹理
具体贴图名(如 ui_icon_gold) 散图,没进图集。大量散图 = DC 大户,应归并进 SpriteAtlas

1.2 "预览是黑的"不等于浪费

Frame Debugger 右侧预览里 mesh 显示为黑色/深色,通常是 mesh 的 UV 落在了图集的透明区域—— 透明像素的 RGB 通道常被填成黑色(alpha=0),预览只画 RGB 不乘 alpha,所以看起来是黑的。

判断方法:看这一批的 顶点数 / 纹理名 / 所属 Canvas,而不是看预览颜色。 真正确认"这一批画了什么"的办法:在事件树里选中该 Draw Mesh,Scene 视图会高亮对应元素。

T5 实例:通行证面板的"黑批次"实际是 sactx-...-atlas-bf6ec7a7 图集里的一块透明区域,不是无效绘制。

1.3 别把 Event 编号当批次序号

Frame Debugger 左侧的 Event #N 是整个帧的事件序号(含相机 clear、ShadowMap、UI 之外的绘制), 不是"UI 第 N 批"。

T5 实例:双相机结构下 Event #6 对应的其实是 UI 的第 1 批(偏移 +5)。 换一台机器、换一个场景结构,偏移量就变了。

正确做法:只统计 Canvas.BuildBatch (<CanvasName>) 节点下面的 Draw Mesh 子项数量,那才是 UI 批次数。

1.4 定位"谁在拆批次"的标准流程

  1. 展开 Canvas.BuildBatch,数子项数量(= 该 Canvas 的批次数);
  2. 逐个点子项,记下每项的 Material + Texture;
  3. 相邻两项若 Material 相同但 Texture 不同 → 图集问题(合图集);
  4. 若 Material 不同 → 看是不是 UIEffect / Mask / 自定义材质导致(本补丁解决的就是 UIEffect 这一类);
  5. 若 Material、Texture 都相同却还是两批 → 中间夹了别的元素打断了层级相邻性(调 hierarchy 顺序)。

二、"帧率不稳"的排查(先分清是真问题还是假象)

2.1 编辑器里的 FPS 数字乱跳 ≠ 真机卡

三个必然存在的干扰因素:

  1. Stats 面板是滚动平均:本身就有延迟和抖动,瞬时值无意义;
  2. 编辑器自身开销:Scene 视图重绘、Inspector GUI repaint、资源导入、Play 模式的 EditorApplication update,都会吃掉帧时间,且不均匀;
  3. vSync 量化:开了垂直同步后帧率只能在 60 / 30 / 20 这些档位跳,看起来像"忽高忽低",其实是量化台阶。

结论:判断帧率问题必须用 Profiler 看帧时间,不要用编辑器 Stats 的 FPS 数字,更不要靠肉眼感觉。

2.2 Profiler 三步法

Window → Analysis → Profiler,勾 CPU Usage,连真机(或编辑器 Play):

第一步:看帧时间尖峰,不看平均值

  • 图表区找突然拉高的柱子(正常 60 帧 = 16.6ms,超过 33ms 就是明显卡顿);
  • 区分"偶发尖峰"(GC、资源加载、实例化)和"持续偏高"(每帧都慢,通常是渲染或 Update 逻辑)。

第二步:展开尖峰帧,找两类元凶

  • GC.Alloc / GC.Collect:内存分配导致的卡顿。看调用栈定位到具体函数;
  • 耗时 Top 函数:常见是 Canvas.SendWillRenderCanvases(UI Rebuild)、SkeletonGraphic.Update(Spine)、Physics、Animator.Update。

第三步:对号入座

Profiler 里看到的 通常原因 处理方向
Canvas.SendWillRenderCanvases 高 UI Rebuild 频繁(Text 改字、SetActive 抖动、布局组件重算) 减少每帧改 UI;避免 LayoutGroup 嵌套;文字更新加脏标记
Canvas.BuildBatch 高 批次太多 / Canvas 太大 降 DC(本包主题);拆 SubCanvas 隔离动态区
GC.Alloc 持续出现 每帧 new / 装箱 / 字符串拼接 / foreach 某些集合 缓存对象;用 struct 枚举;避免运行时 $"..." 拼路径
SkeletonGraphic 相关高 Spine UI 骨骼持续驱动 不可见时禁用组件;降低 update mode 频率
Shader.Parse / Material 创建 材质实例被反复创建 本补丁正是解决 UIEffect 这一类

2.3 帧率设置本身的自查(排除"根本没设 60 帧")

# 在目标工程里搜这几处,确认帧率策略
cd <目标工程>\unity
Select-String -Path (Get-ChildItem -Recurse -Filter *.cs -Exclude *Library*).FullName `
  -Pattern "targetFrameRate|vSyncCount|OnApplicationFocus|OnApplicationPause" |
  Select-Object Path, LineNumber, Line

T5 的实际情况(可作为参考范式):

  • 启动即设:GameInitWrapper.cs → AppUtils.Init(60, true) → Application.targetFrameRate = 60;
  • 真机切后台降帧:Engine.cs 的 OnApplicationFocus,在 #if (UNITY_ANDROID || UNITY_IOS) && !UNITY_EDITOR 内, 失焦 → 10 帧,回前台 → 60 帧。

重要:这个"后台降 10 帧"经常被误读成"帧率不稳"。它只在真机切后台时生效,编辑器里不触发。 排查时先确认这类分支有没有被误命中(比如某些平台宏定义写错,导致编辑器也走进去了)。

2.4 对照实验:怀疑某个组件时,禁用它再测

判断"是不是这个组件在拖帧"的唯一可靠方法是对照实验:

  1. Profiler 开着,记录当前帧时间基线;
  2. 运行时把嫌疑组件 enabled = false(或在 Inspector 里取消勾选);
  3. 看帧时间是否明显下降;
  4. 恢复,确认帧时间回到基线(排除"只是这一帧刚好快了")。

T5 实例:大厅 UI_MainPanel 挂了 sk_hero + sk_pet 两个 SkeletonGraphic(UI Spine), 是"持续负载"最大嫌疑。验证方式就是运行时禁用这两个组件看帧时间变化。 注意:对照实验只做临时禁用,不要顺手删组件——删之前必须 grep 业务代码引用(见 §4)。


三、UI 降 DC 的通用手段清单(按性价比排序)

本包的 configId 补丁只解决"UIEffect 拆批次"这一项。完整的降 DC 手段:

手段 收益 成本 说明
UIEffect 共享材质(本包路线①) 高(同配置特效密集时) 极低(改插件 2 文件,业务零改动) 见 01_UIEffect材质共享降DC/
全屏面板遮挡(本包路线②) 高(全屏页多、下层界面复杂时) 低(挂组件,业务零改动) 见 02_全屏面板遮挡省DC/;有两个生命周期陷阱,必看文档第七章
关掉无用的 URP Feature(本包路线③) 中(T5 实测 SSAO 占 4 DC) 极低(改一个开关) 见 03_渲染设置与资源清理/;2D/UI 场景关 SSAO 无视觉损失
散图归并进 SpriteAtlas 高 中(需美术/UI 配合规划图集) 一个面板的图尽量进同一个图集
统一 UIEffect 配置 中高 低(改 prefab 参数) 补丁生效的前提是"配置相同",美术参数乱调会抵消收益
调整 hierarchy 顺序让同图集相邻 中 低 合批要求层级相邻,穿插即断批
减少 Mask / RectMask2D 中 中 每个 Mask 内部独立成批,跨 Mask 永不合批
拆 SubCanvas 隔离高频变化区 中(降 Rebuild 更明显) 中 动静分离;但 SubCanvas 会各自建批,别拆过头
TMP 字体图集与 Image 分区 低中 低 文字与图片天然不同纹理,布局上让它们各自成组
控制同屏 Spine(SkeletonGraphic)数量 中 中 每个至少独占一批且打断周围 UI 合批;不可见时禁用

四、血泪教训(务必遵守)

  1. 删任何 UI 组件前,先 grep 业务代码有没有引用它。 T5 曾误判"某 UIEffect 没有视觉效果"就删了组件,结果业务代码 GetComponent<UIEffect>() 拿到 null,运行时 NRE。
    # 删组件前的强制动作:确认无引用
    cd <目标工程>\unity\Assets
    Select-String -Path (Get-ChildItem -Recurse -Filter *.cs).FullName `
      -Pattern "UIEffect" | Select-Object Path, LineNumber, Line
    
  2. 不要用肉眼/主观感受下性能结论。"我感觉有点卡"必须先变成 Profiler 里的帧时间数字,再谈优化。
  3. 改插件源码前先确认它不是 PackageCache 只读副本,否则改完 Unity 一刷新就没了(详见手册 §1.1)。
  4. 补丁一定要提交进 git 并写清楚 commit message,插件升级后才知道要重打什么。
  5. 优化改动要单独提交,别和无关改动混在一个 commit 里。 T5 的 c61b9012 优化DC 就混进了 7 个 prefab 的 FullScreenCover 配置改动,导致后来做归档时必须手工拆分 diff。

五、附:UI DC 体检工具设计规格(源码已丢失,可据此重建)

T5 曾有一个自研 Editor 工具 UIDCProbeTool.cs(Assets/Editor/ 下),删除时未提交 git,源码不可恢复。 如果新项目需要比 Frame Debugger 更高效的批量归因工具,按下面规格重建即可(约 800 行,一个下午的工作量):

定位:只读的 EditorWindow,不改场景、不改资源,专门回答"这个面板的 DC 被谁拆了"。

两种模式:

  1. 真实批次模式:Play 模式下遍历目标 Canvas,按 UGUI 的合批规则(材质 + 纹理 + 层级相邻 + 同 Canvas)模拟分批,输出批次列表;批次 index 从 1 开始计数(不是 0,避免与 Frame Debugger 的 Event 编号混淆)。
  2. 材质分组模式:把所有可绘制元素按 材质.GetInstanceID() + 主纹理.GetInstanceID() 归组,每组显示"元素数量 / 涉及对象路径 / 纹理名",直接暴露"哪种材质独占了多少批"。支持按纹理 hash 搜索(比如输入 Frame Debugger 里看到的 bf6ec7a7 反查是哪个图集)。

输出字段(每批一行):批次 index、Canvas 路径、材质名、纹理名、元素数量、是否可合并到前一批、不可合并原因(图集不同 / 材质不同 / 层级被打断 / Mask 隔离)。

界面:顶部选 Canvas(下拉 + Ping 按钮定位)+ 模式切换 + 搜索框;列表分页(每页 100 条);支持导出 CSV(UTF8 BOM,Excel 直接打开不乱码);底部汇总行(总批次 / 可优化批次 / 预估可降到多少)。

关键实现提示:

  • 全程走 AssetDatabase / 反射读 Canvas 内部批次数据时注意团结引擎(Tuanjie)对 guid 的加密,不要依赖 guid 字符串,用 GetInstanceID() 做 key;
  • 只做只读分析,不 SetActive、不改材质、不 Undo 记录;
  • 工具本身放 Assets/Editor/,用完可以整文件删除(记得连 .meta 一起删,否则 Unity 报孤立 meta 警告)。

如果只是想快速定位问题,Unity 自带 Frame Debugger + 本文第一章的方法已经够用,不必重建工具。