# 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-` | **SpriteAtlas 运行时图集**(`sactx` = Sprite Atlas Texture)。`` 标识具体哪个图集,两个批次 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 ()` 节点**下面**的 `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()` 拿到 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 + 本文第一章的方法已经够用**,不必重建工具。