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

176 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 帧")
```powershell
# 在目标工程里搜这几处,确认帧率策略
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。
```powershell
# 删组件前的强制动作:确认无引用
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 + 本文第一章的方法已经够用**,不必重建工具。