12 KiB
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 定位"谁在拆批次"的标准流程
- 展开
Canvas.BuildBatch,数子项数量(= 该 Canvas 的批次数); - 逐个点子项,记下每项的 Material + Texture;
- 相邻两项若 Material 相同但 Texture 不同 → 图集问题(合图集);
- 若 Material 不同 → 看是不是 UIEffect / Mask / 自定义材质导致(本补丁解决的就是 UIEffect 这一类);
- 若 Material、Texture 都相同却还是两批 → 中间夹了别的元素打断了层级相邻性(调 hierarchy 顺序)。
二、"帧率不稳"的排查(先分清是真问题还是假象)
2.1 编辑器里的 FPS 数字乱跳 ≠ 真机卡
三个必然存在的干扰因素:
- Stats 面板是滚动平均:本身就有延迟和抖动,瞬时值无意义;
- 编辑器自身开销:Scene 视图重绘、Inspector GUI repaint、资源导入、Play 模式的 EditorApplication update,都会吃掉帧时间,且不均匀;
- 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 对照实验:怀疑某个组件时,禁用它再测
判断"是不是这个组件在拖帧"的唯一可靠方法是对照实验:
- Profiler 开着,记录当前帧时间基线;
- 运行时把嫌疑组件
enabled = false(或在 Inspector 里取消勾选); - 看帧时间是否明显下降;
- 恢复,确认帧时间回到基线(排除"只是这一帧刚好快了")。
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 合批;不可见时禁用 |
四、血泪教训(务必遵守)
- 删任何 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 - 不要用肉眼/主观感受下性能结论。"我感觉有点卡"必须先变成 Profiler 里的帧时间数字,再谈优化。
- 改插件源码前先确认它不是 PackageCache 只读副本,否则改完 Unity 一刷新就没了(详见手册 §1.1)。
- 补丁一定要提交进 git 并写清楚 commit message,插件升级后才知道要重打什么。
- 优化改动要单独提交,别和无关改动混在一个 commit 里。
T5 的
c61b9012 优化DC就混进了 7 个 prefab 的 FullScreenCover 配置改动,导致后来做归档时必须手工拆分 diff。
五、附:UI DC 体检工具设计规格(源码已丢失,可据此重建)
T5 曾有一个自研 Editor 工具 UIDCProbeTool.cs(Assets/Editor/ 下),删除时未提交 git,源码不可恢复。
如果新项目需要比 Frame Debugger 更高效的批量归因工具,按下面规格重建即可(约 800 行,一个下午的工作量):
定位:只读的 EditorWindow,不改场景、不改资源,专门回答"这个面板的 DC 被谁拆了"。
两种模式:
- 真实批次模式:Play 模式下遍历目标 Canvas,按 UGUI 的合批规则(材质 + 纹理 + 层级相邻 + 同 Canvas)模拟分批,输出批次列表;批次 index 从 1 开始计数(不是 0,避免与 Frame Debugger 的 Event 编号混淆)。
- 材质分组模式:把所有可绘制元素按
材质.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 + 本文第一章的方法已经够用,不必重建工具。