Appearance
场景信号架构治理:40+ 文件的源码级排雷
SR 场景信号在「新路径建好了、老路径没拆完」的状态下跑了很久。我把 40+ 个文件、100+ 个引用点逐行过了一遍,确认这不是设计而是技术债:同一事件 ID 挂两种载荷、读写混用、三套语义并行。最后用一条 L0→L6 加工流水线模型把全链路钉死,并给出四阶段渐进迁移方案。
排查规模40+ 文件 / 100+ 引用点
删老枚举波及29 个文件
并行信号轨3 条
背景
场景信号的演化分四个阶段,每个阶段在当时都合理:
- 老信号时代:Android 只发一个 SignalSrSceneType(100002),值是 ESceneModel 枚举(Driving=1、Charge=2、Pilot=7、P_Gear=8……),场景判定全在 Android 做好,Unity 被动收一个 int。ArtControl、SR 图层、手势相机这些早期业务都直接消费 ESceneModel。
- 产品需求升级:需要组合挡位(100008)、行泊模式(100009)、特殊场景(100010)等多路信号才能判定场景,单个 100002 表达不了,Android 开始同时下发多路细粒度原子信号。
- XSRScenenManager 诞生:Unity 侧新建状态机,聚合 6 路信号在本地算出 XSceneType(LongTermP / Driving / Parking 三种)+ subSceneType,再用 Broadcast 广播。
- 迁移卡住:新路径建好了,老路径没下线。SystemServiceHandler 注释里的 "Plan 3 / Plan 4 / Plan 5" 说明本来是分批迁的,迁到一半停了。14 个老消费者仍在跑,手势/相机模块是另一个团队,还在用老的 sceneModel.OnValueChaned。
历史背景文档自己总结得很到位:不是"设计了两套",而是"新的建好了,旧的没拆完,中间没有适配层,各团队各用各的"。
问题
排查后确认三套信号路径并存,听的是不同的权威字段:
| 轨 | 权威字段 / 事件 | 粒度 | 消费者规模 |
|---|---|---|---|
| A 老业务轨 | Broadcast(30011) / sceneModel | ESceneModel 约 15 种 | 14 个文件 |
| B 新相机状态轨 | Broadcast(30011) / sceneType | XSceneType 3 种 + sub | 2 个文件 |
| C 渲染场景轨 | eXSceneChange(1000010) / ESceneEnum | 6 槽 | 8 个文件 |
三个最扎手的缺陷:
冲突一:同一事件 ID 两种载荷。 事件 30011 同时承载 int(Track A)和 XScene(Track B)两种载荷。CarModel.cs:785-786 和 SRLayerObstacle.cs:134+142 都是同一个类双注册两种监听。底层 ObjEvent.RegistListener 按泛型参数 T 分发——一旦有人把泛型参数写错,回调永远收不到,没有任何报错。这是个静默失败的定时炸弹。
冲突二:读写混用。 GestureZoomBreakService.cs:48 读 sceneType(Track B)做判断,:103 和 :108 又写 sceneModel(Track A)做模拟。读一套写一套,排查问题时看哪个字段都不对。
冲突三:语义不等价。 ESceneModel 有 15 种细分场景(露营 / 小憩 / 舒适都是独立枚举值),XSceneType 只有 3 种大场景,细分要靠 subSceneType 组合表达,且枚举值不对应(SpecialMode.Car_Camping=4 vs ESceneModel.P_Gear_Car_Camping=12)。CarModel 同时维护 originSceneModel、sceneModel、sceneType、subSceneType、gear 五个场景字段,认知负担全压在使用者头上。
技术债量化:直接删 ESceneModel 定义会导致 29 个文件编译报错(Runtime 25 / Editor 3 / Tests 1),约 100+ 引用点。其中 ArtControlLayerSky.cs 有 12 个 case、NapaDisplayManager.cs 有 13 个 case、调试工具 CarModelSignalDebugWindow.cs 有 13 个枚举名映射加 11 个广播调用。而 ESceneModel 还在膨胀——Charge_Settlement_Scene=15 就是后来新加的。
方案
先把全链路钉死:L0→L6 加工流水线
排查中最大的障碍是口径对不齐——Android 同事说"只下发老场景",Unity 侧明明还有第二套态。我把「场景语义被重新计算的次数」作为主轴,把 Android → Unity → 渲染 → 业务串成一条六阶加工流水线:
mermaid
flowchart TD
L0["L0 原始原子量<br>VCU挡位 / PkHmi / Special / LD置信度"]
L1["L1 语义化因子<br>GearSt / PkHmiMode / SpecialSceneType"]
L2["L2 Android 组合<br>SceneType → 100002<br>旁路原子 100008/100009/100010"]
L3A["L3a 老轨 sceneModel<br>Broadcast int 30011<br>Pilot→Driving remap"]
L3B["L3b 新轨 XScene<br>本地再 compose"]
L4["L4 渲染态六槽<br>叠 LD / XPU / P障开关"]
L5["L5 Layer 挂载<br>XSceneManager 按槽位激活"]
L6["L6 业务叠加<br>三轨并行门控"]
L0 --> L1 --> L2
L2 --> L3A
L2 --> L3B
L3A --> L4 --> L5 --> L6
L3B -.->|相机/部分SR直连| L6
L4 -.->|反馈环 LD 态| L3B有了这张图,口径问题迎刃而解:「业务 SceneType 只在 Java 组合」是对的(L2 只有一份官方 SceneType),但原子信号仍在下发,Unity 本地会再 compose 出 XScene——两套说法都成立,只是站在不同阶上说话。这张图还标出了两个排障必须知道的接线偏差:状态机行泊实际听 IXPU_PK_HMI_Mode(111015) 而不是文档写的 100009;LD 业务态听的是渲染派生的 ISYS_LD_Driving_FUNC 反馈环,不是单纯的 100012。
四阶段渐进迁移
一次性删枚举不现实(29 个文件硬阻塞),所以方案的核心是适配层 + 渐进推进:
mermaid
flowchart LR
S1["阶段1 修复危险混用<br>1-2 周"]
S2["阶段2 建立适配层<br>2-4 周"]
S3["阶段3 迁移消费者<br>4-8 周"]
S4["阶段4 废弃老轨<br>1-2 周"]
S1 --> S2 --> S3 --> S4- 阶段1(低风险):修 GestureZoomBreakService 读写混用(统一读写字段),修 SRLayerObstacle 双注册(明确选一套信号)。先把会静默出错的雷排掉。
- 阶段2(中风险):在 SystemServiceHandler.onHandleSceneChanged(:821-834)加 ESceneModel→XScene 映射函数,收到 100002 后同时 Broadcast int(兼容老消费者)和 Broadcast XScene(新路径)。从此 Track B 成为唯一下游信号,新增代码强制走 Track B。Android 端不用改。
- 阶段3(高风险):逐个迁移 Track A 的 14 个消费者,先 ArtControl(3 文件)、SR(6 文件)、Camera(2 文件),最后动 NapaDisplayManager(:223-291,要重构为 XScene→ESceneEnum 映射)。
- 阶段4(收尾):ESceneModel 标记 [Obsolete],清理 CarModel 的 originSceneModel/sceneModel 字段(5 个字段减到 3 个),重写两个调试工具。
关键实现
语义映射表是迁移的硬通货。 大部分老场景可以用组合表达:充电/供电用 LongTermP + SpecialMode,XScene 已有 ISCharging() 这类封装方法;Discharge→Charge 的过滤逻辑在 XSRScene_LongTermPState.CalcuteSubScene() 加一行即可;Pilot→Driving 的 LD 覆盖,Track B 的 GetSceneType() 里 if (isEnterLD) return Driving 已覆盖。真正难还原的是 SelfCar / CheckCar / CarFault / Charge_Settlement,以及 Pilot vs Pilot_Interim 缺 A/X 细粒度、R 档 Android 判 Pilot 而 Unity 新轨倾向 Driving——这几项需要逐个和 Android 对齐语义,是方案里明确标注的专项。
为什么泛型分发会静默失败。 ObjEvent 的注册与广播按 type_index(typeid(T)) 匹配,int 和 XScene 是两个完全不同的 key。CarModel 双注册后两个回调都能收到,看起来"功能正常",但只要任何一处把 RegistListener 的 T 写错,回调就永久落空,编译不报错、运行不崩溃。这也是阶段1 必须先修双注册的原因——双注册把这种错误藏住了。
Track B 的工程底子比 Track A 好。 状态机只有 3 个状态、102 行;计算逻辑在 Unity 侧,可调试可测试;已有端到端测试(DoorOpenGearDCameraSwitchTests.cs),Track A 没有等价测试;SendSceneChanged 日志带 scene + subScene,比 Track A 的裸 int 信息量大;Track A 的 CarModel.cs:1437 还有一句 throw NotImplementedException,Editor 下会直接崩。
时序问题双方都有,别拿它当借口。 维持现状派担心 Track B 的 6 路信号到达有先后会产生中间态。实际上 Track A 的 100002 也是 Android 端 normalSceneTypeJob 用三因子 compose 出来的,同样有时序窗口,只是被封装起来了。Track B 的 isChanged 标记加「结果变化才广播」保证了中间态不产生无效广播,且 compose 在 Unity 本地完成,更可控。
指标
| 指标 | 数值 |
|---|---|
| 排查规模 | 40+ 文件、100+ 引用点 |
| 删 ESceneModel 波及 | 29 个文件(Runtime 25 / Editor 3 / Tests 1) |
| 老轨消费者 vs 新轨消费者 | 14 个文件 vs 2 个文件 |
| CarModel 场景字段 | 5 个 → 迁移后 3 个 |
| 迁移总周期 | 四阶段合计 8-16 周 |
复盘
- 先建模型再动手。 这类跨端技术债最大的成本不是改代码,是口径对不齐。L0→L6 流水线图出来后,"Android 只下发老场景"这类争议一句话就能对齐——双方都站在自己那一阶说话而已。
- 技术债要量化才有推进力。 "双轨并存很乱"说服不了任何人;"删一个枚举波及 29 个文件、100+ 引用点,且枚举还在膨胀"可以。
- 静默失败优先修。 泛型分发写错 T 不报错这种雷,比任何语义分歧都危险——它不会暴露,只会在某个版本突然丢信号。所以阶段1 只做排雷不做迁移。
- 适配层比一次性切换便宜得多。 在 SystemServiceHandler 加映射函数,Android 零改动、老消费者零改动,新路径立刻成为唯一权威源。跨团队改造里,能单边推进的方案才有生命力。
- 这套"源码级排雷 → 量化 → 建模 → 渐进迁移"的方法论,和 Native-Unity 信号链路改造 里白名单收口出口的思路是一脉的:先把流动的东西看清,再谈治理。