24 KiB
24 KiB
UIPrefabCheck 能力清单与优化方案
源文件:
Assets/Editor/UIPrefabCheck.cs(1434 行,本目录含只读副本,MD574D4A63A33869AE560BA6B522E2DABFC) 菜单入口:Tools/UI/Prefab 和图片优化扫描工具、Tools/UI/界面性能巡检评估时间:2026-09-10 | 结论:工具本身设计扎实(只读、分页、LSH 相似图、dHash 带 alpha 包围盒),但对"降 DC"这个目标存在 4 个关键能力缺口,其中 2 个改动量很小、收益很大。
第一章 它现在是什么(两个类,7 个标签页)
一个文件里两个独立部分:
| 类 | 行号 | 职责 | 运行时机 |
|---|---|---|---|
UIRuntimePerformanceAudit |
L17-478 | 界面性能巡检(自动逐个 Show/Close 面板 + 手动录制实际操作) | 必须 Play Mode |
UIPrefabCheck : EditorWindow |
L480-1433 | Prefab / 图片 / 图集静态扫描(6 个页)+ 承载上面那个巡检页 | 编辑模式即可 |
1.1 七个标签页逐项能力
| 标签页 | 绘制方法 | 扫什么 | 判定标准 / 阈值 | 能回答什么问题 |
|---|---|---|---|---|
| 引用明细 | DrawReferencePage L662 |
每个 prefab 里的 Image.sprite / RawImage.texture / SpriteRenderer.sprite |
图片不在 gui/ 或 PSD2UI/UI/ 下 → 标"非标准 UI 资源目录"(IsStandardUiPath L1431) |
某张图被哪个 prefab 的哪个节点用了、属于哪个图集(可点击定位图集) |
| 多预制体共用 | DrawSharedPage L696 |
按 AssetPath 分组,PrefabPaths.Count >= 2 |
— | 哪些图被多个 prefab 共用(共用度高 = 更该进同一个图集) |
| 疑似无用图片 | DrawUnusedImagePage L722 |
UnusedImageRoots(L487-491:gui + PSD2UI/UI)下的 t:Texture2D |
既不在 prefabDependencies(仅来自 prefabRoot 下 prefab 的递归依赖,L895),也没有静态代码引用(HasStaticCodeReference L1148:guid / 全路径 / Resources 相对路径的字面子串) |
哪些图可能是老项目遗留的废图 |
| 节点统计 | DrawNodeStatisticsPage L760 |
每个 prefab 的 Transform 总数 |
超过 GetSuggestedNodeCount L942 的硬编码值(UI面板 150 / UI组件 50 / 特效 80 / 角色 100 / 通用 80)→ 标"建议关注"或"建议拆分" |
哪些 prefab 层级过深/节点冗余(影响 Rebuild 与合批) |
| 大图标记 | DrawLargeImagePage L796 |
UnusedImageRoots 下的 t:Texture2D |
源文件 > 500 KB(L484)或分辨率 ≥ 1024×1024(L485) | 哪些图源文件太大 |
| 相似图 | DrawSimilarImagePage L831 |
UnusedImageRoots 下的 png/jpg/jpeg(L1282) |
dHash 64bit(带 alpha 包围盒裁剪 L1311-1320,再采 8×9 亮度网格 L1323-1333)+ 宽高比差 ≤ 0.05(L1363)+ 汉明距离 ≤ 6(L486);用 8 字节分桶 LSH 避免 O(n²)(FindSimilarImagePairs L1346) |
哪些图是重复/近似重复(老项目遗留的典型症状) |
| 界面性能 | UIRuntimePerformanceAudit.DrawEmbedded L83 |
从 GameInitWrapper 源码正则提取 AddItem<T>(L176)发现所有注册面板 |
自动巡检:预热 10 帧 + 采样 60 帧(L19-20),输出 打开耗时 / 平均 / P95 / 最大帧耗时 / GC 峰值 / 关闭后残留对象数;手动录制:轮询可见面板,记录实际停留期间的帧耗时 | 哪个面板打开慢、哪个面板停留时拖帧、哪个面板关不干净 |
1.2 值得肯定的设计(不要在优化时破坏)
- 全程只读:
PrefabUtility.LoadPrefabContents+finally UnloadPrefabContents(L908-916),不改 prefab、不改图集、不写 Undo。 - dHash 前先做 alpha 包围盒裁剪(L1311-1320)——对 UI 图至关重要:UI 图周围大量透明,不裁剪的话所有图都"相似",误报会淹没结果。这一步做得比很多商业工具细。
- LSH 分桶代替两两比对(L1346-1375):1715 张图两两比对是 147 万对,分桶后只比对同桶候选,扫描时间从分钟级降到秒级。
- 分页 100 条/页 + 筛选 + 可点击 Ping(
DrawPagerL1403、DrawAtlasLinkL974):结果多也不会卡 GUI。 - CSV 导出带 UTF8 BOM(L1426、L458):Excel 直接打开不乱码。
- 性能巡检有"关闭后残留对象"检查(
FinishClosingL416-427):能发现"面板关了但子对象还活着",这正是 T5 FullScreenCover 复活问题的同类症状。
第二章 它对"降 DC"能帮什么、不能帮什么
2.1 能帮(已在 T5 验证过的用法)
- 找图集分散问题:引用明细页每行都显示图集归属,
FindAtlases用atlas.CanBindTo(sprite)(L968)判断,比读 meta 可靠。一个面板里图的图集名越杂,批次越多。 - 找重复图:相似图页直接指出"这两张图其实是一张",合并后既降包体又降批次。
- 找废图:疑似无用图片页(但判定弱,见 §3 B1)。
- 量化面板性能:界面性能页给出 P95 帧耗时,可作为优化前后对比的基线。
2.2 不能帮(4 个关键缺口)
- 测不出 Draw Call / 批次数。
PanelResult(L37-50)只有帧耗时和 GC,降 DC 的成果无法用这个工具量化——改完补丁只能靠 Frame Debugger 手工数,做不了"全项目面板 DC 排行"。 - 看不出"谁在断批"。工具只扫
Image/RawImage/SpriteRenderer三种组件(L912-914),完全没扫Mask/RectMask2D/UIEffect/UIEffectReplica/ 嵌套Canvas/ TMP 字体资产——而这些才是断批的真正元凶。T5 真实案例:
LevelGiftItem.prefab里 1 个UIEffect+ 6 个UIEffectReplica(参数全是默认 None、无视觉效果),运行时每条克隆出 7 个独立材质,直接把面板顶到 101 批。这个工具当时没能报出这个问题。 - 不做合批预估。已经有"每张图属于哪个图集"的数据,却没有按 prefab 聚合出"这个面板涉及几个图集 / 几张散图 / 预估最少批次",无法批量排出"最该优化的面板 TOP N"。
- 不看纹理导入设置。大图页只看源文件大小和分辨率,但真机成本是运行时显存,由
TextureImporter的压缩格式 / maxSize / mipmap / isReadable 决定。一张 1024×1024 的图:RGBA32 未压缩 = 4 MB 显存,ASTC 6×6 = 约 0.5 MB——工具现在看不出这个差别,而 T5 光 png 就有 302 MB / 1715 张。
第三章 优化清单(按优先级,含代码位置与改法)
A 级:能力补齐(对降 DC / 瘦身目标收益最大)
A1 给性能巡检加 DC / 批次 / SetPass 指标 ★首推
- 现状:
StartRecorderL443-448 只起了一个ProfilerRecorder(ProfilerCategory.Memory的GC Allocated In Frame);采样在TickL384-393。 - 改法:照
gcRecorder的同一套写法,多起 3 个 Render 类别的 recorder:在drawCallRecorder = ProfilerRecorder.StartNew(ProfilerCategory.Render, "Draw Calls Count"); batchRecorder = ProfilerRecorder.StartNew(ProfilerCategory.Render, "Batches Count"); setPassRecorder = ProfilerRecorder.StartNew(ProfilerCategory.Render, "SetPass Calls Count");Tick的 Sampling 分支里按现有风格累积(Math.Max(..., recorder.LastValue)取峰值;再加一个累加/计数得平均值);PanelResult(L37)加MaxDrawCalls/AvgDrawCalls/MaxBatches/MaxSetPass四个字段;DrawEmbedded的结果表头(L140-149)与ExportCsv的列(L455-457)同步加列;FinishL440 处一并 Dispose。 - 备选:
UnityEditor.UnityStats.drawCalls / batches / setPassCalls更简单,但受编辑器自身渲染和 Scene 视图影响更大,建议以ProfilerRecorder为主。 - 诚实提示(务必写进界面 HelpBox):编辑器 Play Mode 测得的 DC 含编辑器渲染开销,只能用于同环境下的前后对比,不能当真机绝对值;真机结论仍需 Profiler 连真机复测。这与
05_排查方法论/DC与帧率排查经验.md§2.1 的立场一致。 - 收益:降 DC 从此可量化、可排行、可回归。做完这一项,"改了补丁到底降了多少"不再靠肉眼数 Frame Debugger。
- 工作量:约 30-40 行,半天内含验证。
A2 新增"合批体检"页(静态分析,不需 Play Mode) ★首推
- 现状:
FindAtlasesL962 已能给出每张图的图集归属,但数据只逐行展示,没有按 prefab 聚合;ScanPrefabL906 只遍历三种图形组件。 - 改法:
ScanPrefab里补扫:Mask/RectMask2D数量、UIEffect/UIEffectReplica数量(用GetComponentsInChildren<Coffee.UIEffects.UIEffectBase>(true)可一次拿到两者)、嵌套Canvas数量(GetComponentsInChildren<Canvas>(true).Length - 1)、TMP 文本用到的字体资产 distinct 数;- 新增
BatchHealthRecord:PrefabPath、涉及图集数、散图数(Atlases == "未加入图集")、Mask 数、UIEffect 数、嵌套 Canvas 数、字体种类数、预估最少批次数; - 预估公式(标注为估算,非实测):
预估批次 ≈ distinct图集数 + 散图数 + distinct字体图集数 + UIEffect配置种类数 + Mask分区数 + 嵌套Canvas数 - 新增第 8 个
ViewTab,按"预估批次"降序排列,支持导出 CSV。
- 收益:一次扫描就能列出"全项目最该优化的面板 TOP N"以及每个面板的具体病因(是图集分散、还是 UIEffect 断批、还是 Mask 太多)。这正是丢失的
UIDCProbeTool的核心价值,而且静态就能算、可批量、可导表做优化前后对比,比原来那个只能在运行时看单个 Canvas 的版本更实用。 - 工作量:约 150 行,一天内。
- 依赖注意:引用
Coffee.UIEffects命名空间会让这个 Editor 脚本依赖 UIEffect 包。若担心耦合,可改用GetComponentsInChildren<Component>+GetType().Name字符串判断("UIEffect"/"UIEffectReplica"),零编译依赖。
A3 新增纹理导入设置审计页(服务"源文件瘦身")
- 现状:
ScanLargeImagesL1185 只看GetAssetFileSize(L1215,源文件字节数)和image.width/height。 - 改法:对每张图取
AssetImporter.GetAtPath(path) as TextureImporter,输出并判定:检查项 判定 浪费量级 mipmapEnabledUI 图( textureType == Sprite)开 mipmap = 白扔约 +33% 显存 isReadable非必须读取却开着 = 内存里存两份 ×2 显存 maxTextureSize远大于 prefab 里实际显示尺寸 按平方倍 平台 override 格式 Android/iOS 用 RGBA32 / ARGB32 未压缩 4-8 倍于 ASTC npotScale非 2 的幂图被放大到 2 的幂 最多 4 倍 sRGBTexture/alphaIsTransparency设置与用途不符 视觉问题 同时给出预估运行时显存(宽 × 高 × 每像素字节数,按 format 查表),并与源文件大小并列显示——两者差异常常是数量级的。 - 收益:T5 有 302 MB png / 1715 张 + 65 MB TGA + 11 MB PSD,瘦身真正的肥肉在导入设置而不是源文件。这一页直接指向"改哪个设置省多少 MB"。
- 工作量:约 120 行,一天内。
A4 扩大扫描范围(配置化)
- 现状:
DefaultPrefabRootL482 固定为gui/panel;UnusedImageRootsL487-491 硬编码两个目录(gui+PSD2UI/UI)。T5 的Resources_moved共 739.6 MB,其中effects254.5 MB、res3d202.3 MB、font93.8 MB、animation45 MB 全都扫不到。 - 改法:把两个常量改成可编辑字段 +
EditorPrefs持久化(参考UnusedResourceAuditTool.cs的做法),并支持多目录(string[]);prefabRoot 也支持多个。 - 收益:从"只能体检 UI 目录"变成"能体检全工程"。
- 工作量:约 40 行。
B 级:判定准确性(降低误报/漏报)
B1 "疑似无用图片"漏判动态引用 ★与另一工具冲突,必须处理
- 现状:
HasStaticCodeReferenceL1148-1158 只做三种字面子串匹配:guid、资源全路径、/Resources/之后的相对路径(去扩展名)。DrawUnusedImagePage的 HelpBox(L724-726)已诚实声明"配置表、字符串拼接、反射和运行时下载资源无法可靠识别"。 - 漏判的典型形态(T5 实测存在):
- 配置表驱动:
achievement.json里"icon":"achievement_icon_1"——代码和路径里都没有这个名字; - 代码拼接:
Config_Const.Config_path_ui_task + "btn_go"(T5 有 21 处此类模式); - 插值前缀 + 表数字后缀:
AtlasUtils.cs里$"rarity_{rarity}"、$"img_card{rarity}"、$"phase_{phases}"——资源实际叫rarity_1,语料里永远只有rarity_{rarity}; - Addressables 条目 / Label、场景文件引用、材质与图集 packables 引用。
- 配置表驱动:
- 关键冲突:
Assets/Editor/UnusedResourceAuditTool.cs(菜单Tools/只读资源巡检/未引用资源候选分析)已经实现了完整的 5 层动态识别链(代码字面量 → Resources 拼接 → 语料子串 → 图集/控制器/字体/Spine 配置名 → 插值/拼接前缀+数字后缀),还带 Addressables、场景根、打包可见性分析和三阶段备份流程。两个工具会对同一张图给出矛盾结论(这里说"无用",那里说"疑似动态引用")。 - 改法(推荐第 2 种):
- 最小改动:在
DrawUnusedImagePage的 HelpBox 里加一句指引——"完整分析请用Tools/只读资源巡检/未引用资源候选分析",并把该页改名为"疑似无用图片(弱判定,仅供参考)"; - 彻底改动:把
UnusedResourceAuditTool里的语料构建与前缀命中逻辑抽成一个共享静态类(如DynamicReferenceCorpus),两个工具都调它,结论自然一致。
- 最小改动:在
- 工作量:方案 1 约 5 行;方案 2 约 1 天(含抽取与回归)。
B2 依赖面太窄导致误报
- 现状:
prefabDependenciesL586 只在ScanL895 里由 prefabRoot 下的 prefab 累积(AssetDatabase.GetDependencies(path, true))。默认 prefabRoot =gui/panel。 - 后果:被场景文件、其他目录的 prefab(如
gui/item、effects)、材质、SpriteAtlas 的 packables 引用的图,都会被判成"疑似无用"。 - 改法:根集合补上
t:Scene、全工程t:Prefab(或至少gui全域)、t:Material、t:SpriteAtlas、Addressables 条目——即UnusedResourceAuditTool.CollectRoots的那套。或者干脆按 B1 方案 2 合并。
B3 warningsOnly 在 4 个页无效
- 现状:只有
Matches(L1053,引用明细页)和RefreshFilteredNodeRecords(L1174-1176,节点统计页)判了warningsOnly;RefreshFilteredSharedRecordsL1100、RefreshFilteredUnusedImagesL1160、RefreshFilteredLargeImagesL1227、RefreshFilteredSimilarImagePairsL1391 都只判filter。 - 后果:勾上"仅显示需确认项"后,这 4 个页列表毫无变化,也没有任何提示,容易被误认为"这些页没有问题项"。
- 改法:要么给这些页的 record 补
NeedsConfirm语义并参与过滤,要么在这些页顶部显示一行说明"本页不受'仅显示需确认项'影响"。约 15 行。
C 级:工具自身性能与测量可信度
C1 手动录制每帧全量遍历所有 GameObject ★自测量污染,必须修
- 现状:
TickManualRecordingL258 每帧调用GetVisibleRegisteredPanelsL289,后者执行Resources.FindObjectsOfTypeAll<GameObject>()(L292)——遍历全工程所有 GameObject(含资产、prefab 内的对象,不只是场景对象)。 - 后果:工具自己在拖慢被测帧。手动录制测出来的 P95 帧耗时里包含了这次全量遍历的开销,面板越多、工程越大,污染越严重。这是"测量工具影响测量结果"的经典陷阱,会让所有手动录制数据偏高。
- 改法(三选一,按效果排序):
- 最优:按作者自己在 L301-306 留的 TODO 做——在
MFrame.PanelMgr的Show/completeHandler/Close里加生命周期事件(OnPanelShowRequested/OnPanelShown/OnPanelClosed),工具订阅事件。既零轮询开销,又能拿到真实的"请求 Show → 可见"耗时,不再是一帧近似值; - 次优:轮询降频——加一个
manualPollIntervalSeconds(如 0.1 s),未到间隔就跳过本轮GetVisibleRegisteredPanels;帧耗时采样仍每帧进行,不受影响; - 最省事:把
Resources.FindObjectsOfTypeAll<GameObject>()换成Object.FindObjectsByType<GameObject>(FindObjectsInactive.Exclude, FindObjectsSortMode.None)(只返回场景对象,且跳过排序),并预先把Entries的 prefab 名做成查找表。
- 最优:按作者自己在 L301-306 留的 TODO 做——在
- 工作量:方案 2 约 8 行;方案 3 约 5 行;方案 1 约半天(要动框架代码,需谨慎,属于"改 UI 框架"范畴,动手前应单独确认)。
C2 代码语料是 O(n×m) 全文扫描
- 现状:
ReadAllProjectCodeL1136 把Assets下所有 .cs 拼成一个大字符串(T5 有 4659 个文件,语料是 MB 级);HasStaticCodeReferenceL1150-1157 对每张图做 3 次string.IndexOf全文搜索。1715 张图 = 5145 次 MB 级全文扫描。 - 改法:一次性用正则从语料里提取所有字符串字面量和标识符 token,塞进
HashSet<string>;再对每个资源名/路径做 O(1) 查表 + 前缀匹配。UnusedResourceAuditTool.BuildCodeCorpus已经是这个做法,可直接复用。 - 收益:扫描时间从"十几秒到几十秒"降到"1-2 秒"。
C3 dHash 对全尺寸图取像素
- 现状:
TryGetDifferenceHashL1310image.GetPixels32()取整张解码图的像素,再双重循环遍历每个像素找 alpha 包围盒(L1312-1319)。 - 后果:一张 4096×4096 的图 = 64 MB 托管内存 + 1677 万次循环。相似图扫描在含大图的目录上会明显卡顿甚至触发 GC 尖峰。
- 改法:
ImageConversion.LoadImage解码后,先Graphics.Blit到一张 128×128 的RenderTexture再ReadPixels回小Texture2D,在小图上找包围盒并采样。dHash 最终只取 8×9 = 72 个点(L1323-1333),128² 的精度完全够,而内存从 64 MB 降到 64 KB。 - 注意:包围盒精度会略降(小图上 alpha≤10 的判定 L1316 会因缩放插值而边缘模糊)。稳妥做法是双阶段:先在 128² 上算粗略包围盒,再按该包围盒比例回到原图取那 72 个采样点(只取 72 个像素,不再全图遍历)。
- 工作量:约 30 行。
C4 逐 prefab 递归依赖
- 现状:
ScanL890-896 对每个 prefab 调AssetDatabase.GetDependencies(path, true)(递归全依赖)。566 个 prefab = 566 次递归依赖解析。 - 改法:先收集全部 prefab 路径,一次
AssetDatabase.GetDependencies(string[] pathNames, bool recursive)批量调用(Unity 支持数组重载);或改为GetDependencies(path, false)只取直接依赖 + 自行按prefabDependencies传递闭包。 - 收益:扫描时间下降(幅度取决于依赖深度)。
D 级:UI / 一致性小问题
| 编号 | 现状(代码位置) | 问题 | 改法 |
|---|---|---|---|
| D1 | ExportCsv L1419-1428 |
导出按钮(L624)在任何标签页都可见,但永远只导引用明细(records)。站在"大图标记"页点导出,拿到的却是引用明细表 |
按 activeTab 分派导出内容,或把导出按钮移进各页;至少在非引用明细页禁用并提示 |
| D2 | GetSuggestedNodeCount L942-952 |
节点数建议值全部硬编码(150/50/80/100/80),不同项目的 UI 复杂度差异很大 | 提到常量区并允许界面覆盖 + EditorPrefs 持久化;或按本工程实际分布给分位数建议(如 P90) |
| D3 | 两个 MenuItem L599 / L607 | 两个菜单打开的是同一个 UIPrefabCheck 窗口,只是 activeTab 不同。从"界面性能巡检"进去后点"扫描",会切到静态页,容易迷失 |
用 EditorPrefs 记住上次所在页;或在标题上反映当前页 |
| D4 | DrawUnusedImagePage L732 |
"扫描疑似无用图片"按钮每次都重跑 ReadAllProjectCode(见 C2),连点两次就是两次全量读盘 |
语料缓存 + 失效判断(按 .cs 文件最后修改时间) |
| D5 | Record.Warning L959 |
唯一的告警是"非标准 UI 资源目录",判据是路径前缀(IsStandardUiPath L1431)——这是目录约定告警,不是性能告警 |
配合 A2 增加真正的合批告警(散图、跨图集、UIEffect 断批) |
第四章 如果只做三件事
按"收益 / 工作量"比排序:
| 优先级 | 项目 | 收益 | 工作量 |
|---|---|---|---|
| 1 | A1 性能巡检加 DC / 批次 / SetPass 指标 | 降 DC 成果从"肉眼数 Frame Debugger"变成"可量化、可排行、可回归" | 30-40 行 |
| 2 | A2 新增静态"合批体检"页 | 一次扫描列出全项目最该优化的面板 TOP N + 具体病因(图集分散 / UIEffect / Mask / 嵌套 Canvas) | 约 150 行 |
| 3 | C1 修手动录制的每帧全量遍历(方案 2 或 3) | 消除工具对被测帧的污染,让手动录制数据可信 | 5-8 行 |
其次:B1(避免与 UnusedResourceAuditTool 结论打架)、A3(导入设置审计,服务包体瘦身)、C2/C3(扫描提速)。
第五章 跨项目可移植性(归档前必须知道)
这个工具不是通用工具,有框架绑定:
| 部分 | 依赖 | 能否直接搬到别的项目 |
|---|---|---|
| 6 个静态扫描页(引用明细/共用/无用图/节点/大图/相似图) | 只用 UnityEditor + UnityEngine.UI + UnityEngine.U2D;唯一框架引用是文件头 using MFrame;(L10) |
可以,删掉 using MFrame; 后基本即用(需按项目改 DefaultPrefabRoot L482 与 UnusedImageRoots L487) |
| 界面性能页(自动巡检) | PanelMgr.Instance.Show/Close(L366、L410)+ 从 GameInitWrapper 源码正则解析 AddItem<T>(L172-176) |
不能,必须改成目标项目的面板管理器 API 与注册方式 |
| 界面性能页(手动录制) | ManualPanelNameLookup 由 DiscoverPanels 建立,同样依赖上面两项 |
不能,同上 |
移植时的最小改动清单:
- 删
using MFrame;(L10),确认剩余部分能编译; - 改
DefaultPrefabRoot(L482)与UnusedImageRoots(L487-491)为目标项目的 UI 目录; IsStandardUiPath(L1431)里的两个路径前缀同步改;- 界面性能页:替换
DiscoverPanels(L167)的面板发现方式与PanelMgr.Instance.Show/Close调用(L366 / L410 / L432),或在不需要时整块删掉UIRuntimePerformanceAudit(L17-478)与ViewTab.界面性能(L567)及其分派(L644-647)。
第六章 作者留下的 TODO(原文摘录,L301-306)
工具里已有一段明确的改进计划,做 C1 方案 1 时直接照它执行:
// TODO(精确手动录制耗时):在 MFrame.PanelMgr 中增加 UI 生命周期事件。
// 1. 在 Show(name, data, completeHandler) 入口记录 startTime,并触发 OnPanelShowRequested(name, startTime)。
// 2. 在资源加载完成且 PanelBase._OnShow 执行完成后的 completeHandler 中,触发 OnPanelShown(name, startTime, elapsedMs)。
// 3. 在 Close(name) 成功后触发 OnPanelClosed(name)。
// 4. 本工具订阅上述事件,以 name 关联会话,便可得到真实的"请求 Show 到可见"耗时,
// 不再依赖当前轮询 GameObject 激活状态所得的一帧近似值。
注意:这一项要动
MFrame框架代码(PanelMgr/PanelBase)。T5 有过教训——改 UI 框架前必须确认现有全屏遮挡机制(FullScreenCover)不被破坏,且改动要能被业务面板无感继承。动手前单独确认,不要顺手做。