# 语言房重构方案 ## 文档目的 本文只记录一套可执行的重构方案,本轮不改业务代码。 本次建议的重构范围不是“整套语言房全部推倒重写”,而是优先重构**语言房特效子系统**,重点解决以下问题: - 礼物特效、进场、飞向麦位、房间飘屏并发时主线程抢占严重 - 特效触发源分散,队列和调度分散,后续越优化越难维护 - 房间内特效偶发残留到首页/其它页面,生命周期边界不清晰 - 已做过多轮局部优化,但整体架构仍然偏重,继续打补丁收益会越来越低 本方案的核心原则是: - 尽可能保留当前礼物业务逻辑、素材、阈值和视觉表现 - 先重构特效系统,不一次性推倒 `RTC/RTM/房间业务` 主链路 - 先做“收口与解耦”,再做“性能压测与降级” --- ## 当前建议结论 建议重构方向: - 保留现有房间业务、消息协议、礼物配置接口和大部分现有素材 - 将当前分散在 `VoiceRoomPage / RTM / RTC / OverlayManager / SCGiftVapSvgaManager / GiftAnimationManager / RoomEntranceQueue` 的特效触发与播放逻辑,统一收口到一个**语言房特效引擎** - 把“事件归一化、去重、合并、优先级、预算控制、播放调度、生命周期回收”集中管理 不建议本轮做的事: - 不建议同时重写整套语言房 UI、聊天室、麦位、RTC 状态层 - 不建议先动后端协议 - 不建议一开始就更换全部动画素材格式 --- ## 当前架构现状 ### 当前核心模块 | 模块 | 当前文件 | 当前职责 | 当前问题 | | --- | --- | --- | --- | | 房间主页面 | `lib/modules/room/voice_room_page.dart` | 挂载房间 UI、礼物播报、进场动画、幸运礼物动画、飞向麦位控制器 | 页面层知道太多特效细节,既处理业务又处理动画 | | 房间消息入口 | `lib/services/audio/rtm_manager.dart` | 处理群消息、礼物消息、进场消息、全服广播、飘屏 | 直接触发 `play()`、`OverlayManager().addMessage()`,事件与 UI 强耦合 | | 房间状态入口 | `lib/services/audio/rtc_manager.dart` | 管理进房、退房、麦位、在线用户、房间状态 | 也会直接触发进场坐骑特效,和 RTM 形成双入口 | | 全屏礼物播放器 | `lib/shared/tools/sc_gift_vap_svga_manager.dart` | 管理高成本 `VAP/SVGA` 全屏特效任务 | 只管理自己这一条链路,不是整个房间特效系统的总调度器 | | 房间调度器 | `lib/shared/tools/sc_room_effect_scheduler.dart` | 在高成本特效期间延后部分低优先级特效 | 只有“延后”能力,没有统一队列、优先级、预算、去重、丢弃策略 | | 顶部礼物播报条 | `lib/services/gift/gift_animation_manager.dart` + `lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart` | 4 个槽位的礼物播报、连击合并、幸运礼物奖励框 | 已做局部优化,但仍独立存在,未纳入统一引擎 | | 进场动画 | `lib/ui_kit/widgets/room/anim/room_entrance_screen.dart` | 房内横向 banner 进场队列 | 又是一套独立队列与独立状态机 | | 飞向麦位 | `lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart` | 礼物图片从中间飞向目标麦位 | 独立 overlay、独立队列、独立缓存 | | 飘屏 | `lib/shared/data_sources/sources/local/floating_screen_manager.dart` | 幸运礼物、礼物、火箭、红包、游戏飘屏 | 仍是独立优先队列,与房间特效生命周期分离 | | 旧进场链路 | `lib/ui_kit/widgets/room/anim/room_entrance_widget.dart` | 旧版 `SVGA` 进场队列与 helper | 当前基本不承担主展示,但代码仍存在,增加维护噪音 | ### 当前核心问题 - 同一套语言房里同时存在多套特效队列和状态机 - `RTM/RTC` 直接知道“怎么播动画”,不是只产出事件 - 房间专属特效有一部分挂在根层 `MaterialApp builder`,不是挂在房间路由内部 - 同类事件存在本地触发与远端回流双路径,天然有重复触发风险 - 当前调度器只在高成本特效时“暂缓别的”,但底层仍是多个系统各播各的 - 历史链路未完全收口,例如旧进场 helper 仍保留 --- ## 根因判断 当前卡顿的根因更偏向**结构性问题**,而不是单个控件或单次绘制的问题: 1. 多源头直接驱动 UI `RTM/RTC/RoomPage/GiftSystem` 都可能直接触发动画。 2. 多条高频动画链路并发 全屏 `VAP/SVGA`、顶部礼物条、进场 banner、飞向麦位、房间飘屏可能同时争抢主线程。 3. 房间特效不是一个完整子系统 现在只有局部管理器,没有一个真正拥有“总入口、总队列、总预算、总生命周期”的 runtime。 4. 业务状态更新和特效播放距离太近 房间麦位刷新、在线用户刷新、消息列表更新,会和特效播放共享同一个页面状态环境。 5. 生命周期边界偏松 房间退出后,根层 overlay 和异步回调仍然需要额外兜底清理。 --- ## 重构目标 ### 目标 - 让语言房特效具备统一入口、统一调度、统一生命周期 - 在不改现有礼物业务口径的前提下降低并发卡顿 - 让房间特效只在房间内部存在,不再溢出到其它页面 - 让每种特效都可以独立限流、合并、降级、观测 - 让后续继续优化时,能在统一引擎层处理,而不是继续在各个页面打补丁 ### 非目标 - 本轮不改变房间消息协议 - 本轮不重做礼物页面或礼物发送接口 - 本轮不调整礼物业务规则口径 - 本轮不强制替换所有动画素材格式 --- ## 必须保留的当前礼物/进场业务逻辑 本次重构的前提是:**默认保留当前业务判断口径,只调整结构和调度层**。以下逻辑建议在首轮迁移中原样保留。 | 逻辑项 | 当前口径 | 重构策略 | | --- | --- | --- | | 礼物消息来源 | 继续沿用当前房间 RTM 自定义消息 | 保留,不改协议 | | 全屏礼物触发口径 | 继续按 `gift.special + giftSourceUrl + scGiftHasFullScreenEffect(...)` 判定 | 保留,先抽到策略层 | | 礼物特效总开关 | 继续沿用 `SCGlobalConfig.isGiftSpecialEffects` | 保留,接入新引擎策略层 | | 入场坐骑开关 | 继续沿用 `SCGlobalConfig.isEntryVehicleAnimation` | 保留,接入新引擎策略层 | | 飘屏开关 | 继续沿用 `SCGlobalConfig.isFloatingAnimationInGlobal` | 保留,接入新引擎策略层 | | 顶部礼物播报条合并规则 | 继续按当前 `labelId` 逻辑合并活动项和待播项 | 保留,迁移时保持视觉与合并口径一致 | | 顶部礼物播报条槽位数 | 继续保持 `4` 个槽位 | 保留,首轮不改 | | 顶部礼物播报待播上限 | 继续保持当前上限 `24` | 保留,后续再参数化 | | 顶部礼物播报空闲消失时间 | 继续保持约 `3200ms` | 保留,后续再参数化 | | `customAnimationCount` 逻辑 | 继续作为飞向麦位次数和礼物数量步进动画的依据 | 保留,不改视觉节奏 | | 飞向麦位目标解析 | 继续按当前 `toUser -> account -> 麦位映射` 解析目标 | 保留,迁移时统一封装 | | 飞向麦位会话去重 | 继续保留当前 `queueTag / sessionKey` 级别清理策略 | 保留 | | 幸运礼物里程碑特效 | 继续保留当前里程碑资源集合与触发口径 | 保留 | | 幸运礼物顶部奖励框 | 继续保留当前奖励金额展示方式 | 保留 | | 幸运礼物 burst 条件 | 继续保持“倍率 `>= 10x` 或单次奖励 `> 5000`” | 保留 | | 幸运礼物飘屏口径 | 继续保持当前服务端 `globalNews` + 倍率门槛逻辑 | 保留 | | 大额礼物房间飘屏阈值 | 继续保持当前 `coins > 9999` 才触发房间礼物飘屏 | 保留 | | 礼物触发麦位刷新 | 继续保留礼物触发后的节流刷新逻辑 | 保留,迁移时挪到 effect bridge 或 event side-effect 层 | | 房间进场横幅 | 继续保留当前房内进场 banner 表达方式 | 保留视觉,不一定保留旧实现 | ### 建议保留但从硬编码改为配置化的参数 - 礼物播报槽位数 - 礼物播报待播上限 - 进场队列上限 - 飞向麦位单会话上限 - 高成本特效队列上限 - 飘屏丢弃阈值 - 各类优先级与独占关系 --- ## 目标架构 ### 架构总览 ```mermaid graph TD A[RTM/RTC/房间本地动作] --> B[RoomEffectEventAdapter] B --> C[RoomEffectEngine] C --> D[EffectPolicy] C --> E[Dedup & Merge] C --> F[Lane Dispatcher] F --> G[Fullscreen Lane] F --> H[Entrance Lane] F --> I[GiftTicker Lane] F --> J[SeatFlight Lane] F --> K[Floating Lane] G --> L[RoomEffectStage] H --> L I --> L J --> L K --> L L --> M[SVGA/VAP/Image/Implicit Animations] C --> N[Metrics & Trace] ``` ### 核心思想 - 所有原始事件先进入 `RoomEffectEventAdapter` - adapter 只负责把“原始 RTM/RTC/本地动作”变成**标准化特效事件** - 所有标准化事件再进入 `RoomEffectEngine` - engine 统一做去重、合并、优先级排序、预算判断、降级和分 lane 调度 - 真正的渲染层只负责展示,不再关心业务来源 --- ## 推荐模块拆分图 建议新增一个独立的语言房特效模块,命名可为 `lib/modules/room_effect/` 或 `lib/shared/room_effect/`。 ```text lib/ modules/ room_effect/ domain/ room_effect_event.dart room_effect_task.dart room_effect_lane.dart room_effect_priority.dart room_effect_policy_snapshot.dart application/ room_effect_event_adapter.dart room_effect_engine.dart room_effect_deduplicator.dart room_effect_merger.dart room_effect_policy.dart room_effect_metrics.dart presentation/ room_effect_stage.dart layers/ room_fullscreen_effect_layer.dart room_entrance_effect_layer.dart room_gift_ticker_layer.dart room_seat_flight_layer.dart room_floating_effect_layer.dart infrastructure/ room_effect_asset_preloader.dart room_effect_player_bridge.dart room_effect_lifecycle_guard.dart ``` ### 各模块职责 | 模块 | 职责 | 说明 | | --- | --- | --- | | `RoomEffectEventAdapter` | 事件归一化 | 把 `RTM/RTC/RoomPage` 的原始数据转为统一事件对象 | | `RoomEffectEngine` | 统一调度核心 | 做入队、去重、合并、优先级、预算、丢弃、lane 分发 | | `RoomEffectPolicy` | 播放策略 | 根据机型、开关、 backlog、当前播放状态决定播什么、延后什么、降级什么 | | `RoomEffectStage` | 页面内特效承载层 | 只在语言房路由内部挂载,房间退出就一并销毁 | | `PlayerBridge` | 播放器桥接层 | 首轮复用现有 `SCGiftVapSvgaManager` / `GiftAnimationManager` / 现有动画组件 | | `AssetPreloader` | 资源预热 | 统一预热图片、SVGA、VAP,本轮不再让每个特效组件各自处理 | | `Metrics` | 指标与日志 | 记录入队数、丢弃数、等待时长、播放耗时、异常和 backlog | --- ## 目标事件模型 建议统一定义一个标准化事件对象 `RoomEffectEvent`,至少包含以下信息: - `eventId` - `roomId` - `eventType` - `source` - `createdAt` - `priority` - `dedupKey` - `mergeKey` - `userId` - `targetUserId` - `giftId` - `payload` - `requiresHeavyRenderer` - `roomScoped` ### 推荐事件类型 - `roomUserJoin` - `roomMountEntrance` - `giftTicker` - `giftFullscreen` - `giftSeatFlight` - `giftFloating` - `luckyGiftMilestone` - `luckyGiftRewardTicker` - `luckyGiftBurst` - `rocketFloating` - `redPacketFloating` - `gameFloating` ### 为什么要有标准化事件层 - 后续所有去重和合并都建立在统一事件模型上 - 可把“同一礼物事件触发 3 种视觉表现”拆成 3 个子任务,而不是让 RTM 直接调用 3 个 UI 系统 - 可支持本地自发事件和远端回流事件的统一去重 --- ## 目标任务模型 事件进入引擎后,不直接播放,而是先变成 `RoomEffectTask`。 一个 task 建议至少包含: - `taskId` - `lane` - `priority` - `enqueueAt` - `deadline` - `exclusive` - `mergeable` - `mergeKey` - `dropPolicy` - `payload` - `onStart` - `onComplete` - `onCancel` ### 推荐的 lane 划分 | Lane | 作用 | 特点 | | --- | --- | --- | | `fullscreen` | 全屏礼物、幸运礼物 burst、进场坐骑等高成本特效 | 可独占,优先级最高 | | `entrance` | 房间进场 banner | 串行,可合并,可被延后 | | `giftTicker` | 顶部礼物播报条 | 维持 4 槽位模型 | | `seatFlight` | 飞向麦位 | 可批量、可会话级去重 | | `floating` | 房间飘屏、幸运礼物飘屏、火箭、红包、游戏 | 可按消息类型和房间范围过滤 | --- ## 页面挂载方式调整 ### 当前问题 当前房间特效有一部分挂在根层 `MaterialApp builder`,例如: - 全屏 `VapPlusSvgaPlayer` - `RoomGiftSeatFlightOverlay` 这意味着: - 特效承载层不完全属于房间页面 - 房间退出后需要额外兜底清理 - 根层 overlay 容易被误复用到非房间页面 ### 目标方式 推荐新增 `RoomEffectStage`,只挂在语言房页面内: ```mermaid graph TD A[VoiceRoomPage] --> B[RoomScene] A --> C[RoomEffectStage] C --> D[Fullscreen Layer] C --> E[Entrance Layer] C --> F[GiftTicker Layer] C --> G[SeatFlight Layer] C --> H[Floating Layer] ``` ### 结果 - 房间特效生命周期跟随 `VoiceRoomPage` - 退房、最小化、切房时统一收口 - 根层只保留真正全局的东西,房间特效不再越界 --- ## 首轮推荐复用现有实现的方式 为尽量保留当前礼物逻辑,首轮不需要把所有 renderer 重写,可以先“新引擎 + 旧 renderer”。 ### 可复用项 | 现有实现 | 首轮建议 | | --- | --- | | `SCGiftVapSvgaManager` | 作为 `fullscreen lane` 的底层播放器桥接 | | `GiftAnimationManager` | 作为 `giftTicker lane` 的底层 4 槽位实现 | | `RoomGiftSeatFlightOverlay` | 作为 `seatFlight lane` 的底层渲染器 | | `RoomAnimationQueueScreen` | 其视觉可复用,但队列控制迁到新引擎 | | `OverlayManager` | 首轮可继续承载非房间全局类型;房间类型逐步迁出 | ### 不建议继续保留为长期方案的实现 | 现有实现 | 建议 | | --- | --- | | `room_entrance_widget.dart` 的旧 helper/queue | 清理掉,不再作为正式链路 | | 在 `RTM/RTC` 中直接 `play()` 或 `addMessage()` | 改为统一投递特效事件 | | 在根层 `MaterialApp builder` 挂房间专属特效层 | 迁回房间路由内部 | --- ## 调度与降级策略 ### 首轮策略 首轮重构不建议改业务口径,但建议统一调度策略。 #### 建议规则 - `fullscreen lane` 播放时,`entrance / floating / seatFlight` 默认延后 - `giftTicker lane` 继续独立展示,但只保留当前 4 槽位,不无限堆积 - `seatFlight lane` 保持当前 `customAnimationCount` 逻辑,但由引擎统一裁剪上限 - `entrance lane` 超过 backlog 后可合并为“X 人进入房间” - `floating lane` 在高峰时优先保留高优先级类型,低优先级直接丢弃 ### 第二阶段可再加的自适应策略 - backlog 超阈值时,把同礼物多次飞向麦位降成 1 次飞行 + 数量角标 - 进场队列过长时,多条普通进场合并成一条摘要 - 同时存在全屏高成本特效时,房间礼物飘屏暂停或只保留 1 条 - 低性能设备继续保留现有总开关,但高性能设备也可按瞬时 backlog 动态降级 --- ## 迁移顺序 建议按“先收口入口,再迁移渲染,再清理历史代码”的顺序做,避免一口气全动。 ### 阶段 0:建立基线与指标 目标: - 先补齐特效侧可观测性 - 固化当前礼物逻辑和关键参数,形成“迁移不改口径”的基线 本阶段内容: - 统计每类特效的入队数、丢弃数、平均等待时长、播放时长 - 统计 `fullscreen/entrance/giftTicker/seatFlight/floating` 的 backlog 峰值 - 用 `FrameTiming` 记录语言房高压场景的 build/raster 情况 本阶段不改: - 不改现有播放逻辑 - 不改现有视觉 - 不改现有业务阈值 ### 阶段 1:统一事件入口 目标: - 引入 `RoomEffectEventAdapter` - 所有房间特效先变成统一事件对象 本阶段内容: - 把 `RTM/RTC/VoiceRoomPage/GiftSystem` 里直接触发特效的地方改为“投递事件” - 本地触发与远端回流先统一走一层去重 key 本阶段不改: - renderer 先不迁 - `SCGiftVapSvgaManager`、`GiftAnimationManager` 等先照旧使用 关键收益: - 从这一阶段开始,特效来源就被收口了 ### 阶段 2:落地 `RoomEffectEngine` 目标: - 让所有统一事件进入引擎 - 引擎接管优先级、去重、合并和 lane 分发 本阶段内容: - 新建 `RoomEffectEngine` - 新建 lane 模型 - 先做不改变视觉的“旧 renderer 包装” 本阶段不改: - 不改具体动画 UI - 不改素材 关键收益: - 从“多套队列系统”变成“一个总引擎 + 多条 lane” ### 阶段 3:把房间特效挂回房间路由内部 目标: - 新建 `RoomEffectStage` - 把房间专属层从根层迁回 `VoiceRoomPage` 本阶段内容: - `RoomEffectStage` 承载房间内所有特效 layer - 根层只保留真正全局的内容 - 统一退房清理、最小化清理、切房清理 本阶段重点回归: - 房间退出后不再有礼物图残留 - 房间飘屏不再跑到首页 ### 阶段 4:迁移 `fullscreen lane` 目标: - 先把最高成本、影响最大的特效链路收口 本阶段内容: - `SCGiftVapSvgaManager` 改为由 `fullscreen lane` 调用 - 进场坐骑、全屏礼物、幸运礼物 burst 都走统一 lane - 自带独占、优先级和预热策略 本阶段必须保持不变: - 当前全屏礼物触发口径不变 - 当前幸运礼物里程碑和 burst 口径不变 ### 阶段 5:迁移 `entrance lane` 目标: - 收口房间进场 banner - 清理旧进场链路 本阶段内容: - `RoomAnimationQueueScreen` 只保留渲染,队列逻辑迁到 `entrance lane` - 清理 `room_entrance_widget.dart` 旧 helper 及残余引用 - 引入本地与远端进场事件去重 本阶段必须保持不变: - 当前进场 UI 表达不变 - 当前入场后坐骑与横幅仍可共存,但由统一引擎调度 ### 阶段 6:迁移 `giftTicker lane` 目标: - 保留当前 4 槽位礼物播报条逻辑,但把队列和状态收回引擎 本阶段内容: - `GiftAnimationManager` 改为 lane 内 renderer bridge - 继续保留当前 `labelId` 合并、4 槽位、空闲消失时间、数量步进动画 - 幸运礼物奖励框仍保留当前样式 本阶段必须保持不变: - 连击合并口径 - `customAnimationCount` 对数量展示的影响 - 幸运礼物奖励框逻辑 ### 阶段 7:迁移 `seatFlight lane` 目标: - 把飞向麦位从独立控制器改为统一 lane 管理 本阶段内容: - 复用现有 `RoomGiftSeatFlightOverlay` 作为渲染层 - `queueTag/sessionKey` 的批量裁剪和清理迁入 engine - 目标麦位 key 解析改成统一桥接 本阶段必须保持不变: - 当前会话级去重逻辑 - 当前 `customAnimationCount` 粒度 - 当前图片预热行为 ### 阶段 8:迁移 `floating lane` 目标: - 收口房间礼物飘屏、幸运礼物飘屏、火箭、红包、游戏飘屏 本阶段内容: - 房间内的 floating 类型全部走 `floating lane` - 真正全局广播的类型与房间类型分开 - `OverlayManager` 只保留全局用法,房间类型从中剥离 本阶段必须保持不变: - 幸运礼物 `globalNews` 逻辑不变 - 房间礼物 `coins > 9999` 才飘屏的逻辑不变 ### 阶段 9:清理历史实现与补充自适应降级 目标: - 删掉双轨实现 - 把当前“局部优化痕迹”收成统一结构 本阶段内容: - 删除旧进场 helper - 删除 `RTM/RTC` 中直接 `play()` 和 `addMessage()` 的旧入口 - 完成 backlog 降级策略与观测面板 --- ## 关键兼容策略 ### 1. 先换“控制面”,不先换“渲染面” 首轮不要把所有动画组件重写掉。 推荐策略: - 新引擎先接管“什么时候播、播哪条、谁先谁后、是否丢弃” - 旧组件先继续负责“怎么画出来” 这样做的好处: - 视觉回归风险小 - 礼物逻辑更容易保持一致 - 每一阶段都可以独立回滚 ### 2. 消息协议不动 当前房间消息、礼物消息、幸运礼物消息结构先不改。 这样可以: - 避免前后端联动阻塞 - 避免把“性能问题”变成“协议问题” ### 3. 参数冻结后再迁移 建议先把以下参数冻结成一份配置文档或常量表: - 幸运礼物里程碑列表 - 幸运礼物 burst 条件 - 房间礼物飘屏阈值 - 顶部礼物播报条槽位与时长 - 飞向麦位上限 - 进场队列上限 这样迁移时只改结构,不改结果。 --- ## 风险点列表 | 风险点 | 说明 | 风险等级 | 缓解方式 | | --- | --- | --- | --- | | 本地事件与远端回流重复 | 自己送礼、自己进房、进场坐骑等容易双触发 | 高 | 引入 `dedupKey`,按 `roomId + type + sender + target + gift + timeWindow` 去重 | | 去重过度导致特效漏播 | 某些高频连击若 key 设计过粗,会把应播事件吃掉 | 高 | 区分 `dedupKey` 和 `mergeKey`,先留日志,初期采用保守去重 | | 房间退出后仍有旧特效回调 | `postFrame / delayed / timer / precache` 都可能晚到 | 高 | 所有 task 和 layer 引入 `sessionToken` 或 `roomLifecycleToken` 校验 | | 渲染层迁移后层级错乱 | 根层迁到房间层后,z-order 可能改变 | 中 | 在 `RoomEffectStage` 中明确 layer 顺序,先对齐现有视觉层级 | | 资源预热过多导致内存抖动 | 统一预热后若没有预算控制,容易顶高内存 | 中 | 按 lane、资源类型、尺寸做分层缓存和 LRU | | 幸运礼物口径漂移 | 这块逻辑复杂,回归风险高 | 高 | 首轮冻结参数与触发口径,不做业务变更 | | 飞向麦位目标丢失 | 目标座位 GlobalKey 尚未可用时可能丢动画 | 中 | 保留当前重试机制,但统一纳入 lane,限制重试次数 | | 飘屏房间范围判断错误 | 房间飘屏与全局飘屏容易混 | 中 | 明确 `roomScoped` 与 `globalScoped` 两类事件 | | 迁移阶段代码并存时间过长 | 双轨逻辑存在越久,后续越难收口 | 中 | 每一阶段都指定“完成后可删”的旧文件或旧入口 | | 测试面过大 | 礼物、幸运礼物、火箭、红包、进场、坐骑交叉很多 | 高 | 用阶段化回归清单,按 lane 逐项验收 | --- ## 回归测试清单 ### 礼物链路 - 单次送礼 1 次 - 同一礼物连续送 5 次、10 次 - `customAnimationCount > 1` 的批量场景 - 幸运礼物普通中奖 - 幸运礼物高倍率中奖 - 同时出现全屏礼物 + 顶部播报条 + 飞向麦位 ### 进场链路 - 自己进入房间 - 别人进入房间 - 别人带坐骑进入房间 - 同时 5 人以上快速进入 ### 飘屏链路 - 房间礼物飘屏 - 幸运礼物房间飘屏 - 全局广播幸运礼物飘屏 - 火箭飘屏 - 红包飘屏 ### 生命周期 - 房间内最小化 - 退出房间 - 切到首页/消息/我的 - 从 A 房切到 B 房 ### 性能 - 普通机型连续送礼 10 秒 - 低性能机型连续送礼 10 秒 - 进场和礼物叠加压测 - 房间内停留 10 分钟观察缓存与内存变化 --- ## 验收指标建议 建议在重构过程中记录以下指标,避免“感觉快了”但没有客观标准: - 语言房高压场景下 `FrameTiming` 的 `build/raster` P95 - `fullscreen lane` 平均等待时长 - `entrance/floating/seatFlight` backlog 峰值 - 被丢弃任务数量和占比 - 同事件重复触发数 - 房间退出后残留特效数 ### 目标方向 - 房间高压场景下明显减少整页卡顿和掉帧 - 不再出现房间特效溢出到首页 - 不再依赖在多个管理器里各自补清理逻辑 - 新增一个特效类型时,只需要走“标准化事件 -> lane -> layer”链路 --- ## 预计实施节奏 如果只重构语言房特效子系统,不推倒整套房间业务,建议节奏如下: | 阶段 | 预估工作量 | | --- | --- | | 阶段 0 ~ 2 | 2 到 3 天 | | 阶段 3 ~ 5 | 4 到 6 天 | | 阶段 6 ~ 8 | 4 到 6 天 | | 阶段 9 与回归 | 2 到 3 天 | 整体建议: - 保守估计约 `2` 周 - 如果中途顺手一起重构 `RTC/RTM/RoomPage` 主业务结构,工期会明显变长 --- ## 多线程并行执行方案 如果后续不是单线程顺着做,而是你准备把这次重构拆成多个并行线程分别跑,建议采用**“3 个主线程并行 + 1 个收尾线程串行收口”**的方案。 ### 并行收益预估 按“最稳的预期”估算: - 单线程完整落地:约 `20 到 24` 小时 - 多线程并行后:约 `14 到 16` 小时 这里的前提是: - 先冻结接口命名和模块边界 - 尽量避免多个线程同时改同一批现有文件 - 最后仍保留一个统一收尾和回归线程 不建议的并行方式: - 不建议 3 个线程同时直接改 `rtm_manager.dart / rtc_manager.dart / voice_room_page.dart` - 不建议一边改 engine,一边让多个线程各自发明自己的事件模型 - 不建议一开始就多个线程同时删旧链路 ### 并行原则 1. 先统一接口,再并行开发 至少先约定好以下对象名和职责: - `RoomEffectEvent` - `RoomEffectTask` - `RoomEffectLane` - `RoomEffectEngine` - `RoomEffectStage` - `RoomEffectEventAdapter` 2. 新文件优先并行,旧文件尽量收口到单一线程 新增模块最适合多线程拆分;旧有入口文件应尽量减少多人同时修改。 3. 先搭骨架,再接现有链路 先让 engine 与 stage 的框架稳定,再开始把 `RTM/RTC/VoiceRoomPage` 接上去。 4. 收尾一定要串行 旧链路删除、层级调整、房间生命周期回收、回归修 bug,这些必须收口到最后一个线程统一处理。 --- ### 推荐线程划分 #### 线程 A:Engine 基础层 定位: - 负责“语言房特效引擎”的核心模型与调度骨架 推荐负责范围: - `lib/modules/room_effect/domain/` - `lib/modules/room_effect/application/` - `lib/modules/room_effect/infrastructure/` 中和调度、预热、生命周期守卫相关的文件 推荐文件所有权: - `room_effect_event.dart` - `room_effect_task.dart` - `room_effect_lane.dart` - `room_effect_priority.dart` - `room_effect_engine.dart` - `room_effect_policy.dart` - `room_effect_deduplicator.dart` - `room_effect_merger.dart` - `room_effect_metrics.dart` - `room_effect_asset_preloader.dart` - `room_effect_lifecycle_guard.dart` 本线程不建议碰: - `rtm_manager.dart` - `rtc_manager.dart` - `voice_room_page.dart` - 现有具体动画 widget 的渲染实现 本线程交付物: - 可编译的 engine/domain/application 基础骨架 - lane、task、event、policy 的统一接口 - 基本的 backlog、优先级、去重、合并接口 #### 线程 B:Stage 与渲染承载层 定位: - 负责“语言房特效舞台”和各 layer 的承载结构 推荐负责范围: - `lib/modules/room_effect/presentation/` 推荐文件所有权: - `room_effect_stage.dart` - `layers/room_fullscreen_effect_layer.dart` - `layers/room_entrance_effect_layer.dart` - `layers/room_gift_ticker_layer.dart` - `layers/room_seat_flight_layer.dart` - `layers/room_floating_effect_layer.dart` - 如果需要,可新增 presentation 侧 controller 或 snapshot 文件 本线程可复用但不建议大改的旧文件: - `lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart` - `lib/ui_kit/widgets/room/anim/room_entrance_screen.dart` - `lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart` - `lib/ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart` 建议策略: - 先做 layer 容器和接口桥接,不先重写底层动画实现 - 首轮允许通过 bridge 调现有 renderer 本线程不建议碰: - `rtm_manager.dart` - `rtc_manager.dart` - `main.dart` 本线程交付物: - `RoomEffectStage` - 各特效 layer 的挂载顺序和层级 - 渲染层与 engine 的消费接口 #### 线程 C:旧链路入口收口与适配层 定位: - 负责把现有 `RTM/RTC/VoiceRoomPage/GiftSystem` 的特效触发点统一改成事件投递 推荐负责范围: - `lib/services/audio/rtm_manager.dart` - `lib/services/audio/rtc_manager.dart` - `lib/modules/room/voice_room_page.dart` - `lib/services/gift/gift_system_manager.dart` - 适配层文件,例如: - `lib/modules/room_effect/application/room_effect_event_adapter.dart` - 或单独的 bridge/wiring 文件 本线程重点任务: - 找出所有直接 `play()`、`OverlayManager().addMessage()`、直接入队的触发点 - 改为投递标准化事件 - 做本地触发和远端回流的去重 key - 保持现有礼物业务逻辑不变 本线程不建议碰: - `RoomEffectStage` 内部结构 - 各 layer 的视觉实现 - 大量历史文件删除 本线程交付物: - 统一事件入口 - `RTM/RTC` 不再直接知道“怎么播动画” - 旧业务逻辑已迁到 adapter/event 侧表达 #### 线程 D:收尾、清理与回归 定位: - 在 A/B/C 合并后串行执行,不建议一开始就启动 推荐负责范围: - `lib/main.dart` - `lib/modules/room/voice_room_page.dart` - `lib/shared/data_sources/sources/local/floating_screen_manager.dart` - `lib/shared/tools/sc_room_effect_scheduler.dart` - `lib/ui_kit/widgets/room/anim/room_entrance_widget.dart` - 其它需要删除旧逻辑、改挂载层级、补回归日志的文件 本线程重点任务: - 把房间专属特效层从根层尽量收回房间页内部 - 清理旧进场 helper 和双轨实现 - 清理失效入口 - 跑一轮回归,修明显联调问题 --- ### 最稳的执行顺序 推荐按下面顺序跑,而不是 4 个线程同时无序开工。 #### 第 1 步:接口冻结 由任意一个线程先完成,建议控制在 `0.5 到 1` 小时内。 需要先冻结的内容: - `RoomEffectEvent` 字段 - `RoomEffectTask` 字段 - lane 列表 - engine 对外接口 - stage 对外接口 - adapter 的输入输出职责 这一步完成后,线程 A/B/C 才正式并行。 #### 第 2 步:A/B/C 三线程并行 并行期间建议这样分: - A 只写 engine/domain/application 基础层 - B 只写 stage/presentation/layer 容器 - C 只写现有旧入口收口和事件适配 并行期间禁止多人同时改的文件: - `lib/services/audio/rtm_manager.dart` - `lib/services/audio/rtc_manager.dart` - `lib/modules/room/voice_room_page.dart` - `lib/main.dart` 这些文件建议默认归线程 C 或线程 D 所有。 #### 第 3 步:先合 A,再合 B,再合 C 推荐合并顺序: 1. 先合线程 A 先把 engine 和模型定住,避免后续接口继续漂。 2. 再合线程 B 让 stage 和 layer 按 A 的接口接上。 3. 再合线程 C 最后把现有旧入口真正接到 engine 上。 这样做的原因: - 可以最大程度减少 `A/B/C` 三方接口对撞 - 让旧链路接入时面对的是已经稳定的 engine 与 stage #### 第 4 步:最后由线程 D 串行收尾 线程 D 再做这些事: - 根层与房间层的挂载调整 - 删除旧实现 - 生命周期清理 - 房间退出和切房回归 - backlog 观测和简单日志补充 --- ### 推荐的文件写入边界 为了减少合并冲突,建议严格遵守下面的写入边界。 | 线程 | 允许重点修改 | 尽量不要修改 | | --- | --- | --- | | A | `lib/modules/room_effect/domain/`、`application/`、`infrastructure/` 新文件 | 现有房间业务文件 | | B | `lib/modules/room_effect/presentation/` 新文件 | `rtm_manager.dart`、`rtc_manager.dart` | | C | `rtm_manager.dart`、`rtc_manager.dart`、`voice_room_page.dart`、`gift_system_manager.dart`、adapter | `main.dart`、presentation 新文件的大结构 | | D | `main.dart`、旧 helper 清理、根层挂载调整、遗留逻辑删除 | engine 核心接口 | --- ### 推荐的并行里程碑 建议把并行执行拆成 4 个里程碑,每个里程碑都要求能单独检查。 #### 里程碑 1 - A:engine/domain/task/lane 骨架完成 - B:stage/layer 容器骨架完成 - C:梳理所有旧触发点,列出事件映射表 #### 里程碑 2 - A:lane 调度、去重、合并接口可用 - B:各 layer 能消费统一 task 或 snapshot - C:`RTM/RTC/VoiceRoomPage` 已改成先投递事件,不直接触发动画 #### 里程碑 3 - A/B:engine 与 stage 已能联通 - C:主要礼物/进场/飘屏入口已接新引擎 #### 里程碑 4 - D:旧链路清理、房间退出/切房回归完成 --- ### 不同并行强度的建议 #### 最稳方案 - `3` 个主线程并行 - `1` 个收尾线程后置串行 适用场景: - 你希望尽量减少互相撞车 - 你更在意一次性落稳,而不是追求最短墙钟时间 #### 更激进方案 - `4` 个线程一开始就并行 不太建议,原因是: - `main.dart` - `voice_room_page.dart` - `rtm_manager.dart` - `rtc_manager.dart` 这些文件很容易形成高冲突区,激进并行最后未必更快。 --- ### 最终建议的并行执行口径 如果你后面准备“分别跑”多个线程,建议直接按下面口径执行: - 线程 A:只做 engine/domain/application 基础层 - 线程 B:只做 stage/presentation/layer 承载层 - 线程 C:只做旧入口收口和事件适配 - 线程 D:最后统一清理旧链路、改挂载、跑回归 这是这次语言房特效系统重构里,**最稳、最不容易互相打架**的一种并行拆法。 --- ## 可直接投喂 Codex 的执行 Prompts 下面这组 prompt 是给你后面开多个 Codex 线程时直接复制使用的。 推荐使用方式: 1. 先把“公共约定 prompt”复制给每个线程 2. 再把对应线程的专属 prompt 追加给该线程 3. 线程 D 不要一开始就跑,等 A/B/C 合并后再启动 ### 公共约定 Prompt 建议把下面这段先发给每一个线程,作为共同上下文。 ```text 你现在在 /Users/nigger/Documents/GitHub/chatapp3-flutter 仓库中工作。 当前目标不是重构整个语言房,而是优先重构“语言房特效子系统”。 请先阅读根目录文档: - /Users/nigger/Documents/GitHub/chatapp3-flutter/语言房重构.md 本轮必须遵守这些硬约束: 1. 尽量保留当前礼物业务逻辑、阈值和视觉表现,不要擅自改业务口径。 2. 不要改房间消息协议,不要改后端接口。 3. 只在你被授权的写入范围内改文件,不要碰其它线程的主要负责文件。 4. 不要大规模格式化无关文件,不要顺手重构无关模块。 5. 如果你发现某个接口命名需要和其它线程统一,请优先遵守本文档中的统一命名,不要自行发明第二套。 统一命名约定如下: - RoomEffectEvent - RoomEffectTask - RoomEffectLane - RoomEffectPriority - RoomEffectEngine - RoomEffectPolicy - RoomEffectEventAdapter - RoomEffectStage 统一 lane 命名如下: - fullscreen - entrance - giftTicker - seatFlight - floating 必须保留的当前关键逻辑: - 顶部礼物播报条继续保持 4 个槽位 - 顶部礼物播报条待播上限继续按当前 24 处理 - 顶部礼物播报条空闲消失时长继续保持约 3200ms - 飞向麦位继续保留 customAnimationCount 的粒度逻辑 - 幸运礼物 burst 继续保持“倍率 >= 10x 或单次奖励 > 5000” - 房间礼物飘屏继续保持 coins > 9999 才触发 - 继续沿用当前特效总开关: - SCGlobalConfig.isGiftSpecialEffects - SCGlobalConfig.isEntryVehicleAnimation - SCGlobalConfig.isFloatingAnimationInGlobal 完成后请在回复中明确给出: - 改了哪些文件 - 你负责范围内已经完成了什么 - 还依赖其它线程提供什么 - 有没有你刻意没有动的风险点 ``` --- ### 线程 A Prompt 用途: - 负责 engine/domain/application 基础层 直接投喂: ```text 在遵守“公共约定 prompt”的前提下,执行线程 A 的任务。 你的职责是:搭建语言房特效系统的 engine/domain/application 基础层,只负责新架构骨架,不接老的 RTM/RTC 入口,不改现有房间业务触发逻辑。 你的主要目标: 1. 新增语言房特效模块基础目录,优先建议放在: - lib/modules/room_effect/domain/ - lib/modules/room_effect/application/ - lib/modules/room_effect/infrastructure/ 2. 建立并实现以下核心对象或等价对象: - RoomEffectEvent - RoomEffectTask - RoomEffectLane - RoomEffectPriority - RoomEffectPolicy - RoomEffectEngine - RoomEffectDeduplicator - RoomEffectMerger - RoomEffectMetrics - RoomEffectLifecycleGuard - RoomEffectAssetPreloader 3. engine 至少要具备这些能力: - 接收 RoomEffectEvent - 转换为 RoomEffectTask - 按 lane 分发 - 基于 priority 排序 - 留出去重、合并、丢弃、延后、独占的接口 - 能产出每个 lane 的可消费状态或流 4. 你可以先做“最小可用骨架”,但不要只写空壳文件。至少保证接口能表达真实系统能力。 5. 你需要把这些现有业务约束沉淀到 policy 或 constants 中,但不要改业务阈值: - giftTicker 4 槽位 - pending 24 - idle dismiss 3200ms - burst 条件 - 飘屏阈值 你的明确写入范围: - 可以新增和修改: - lib/modules/room_effect/domain/** - lib/modules/room_effect/application/** - lib/modules/room_effect/infrastructure/** 你的禁止写入范围: - 不要修改: - lib/services/audio/rtm_manager.dart - lib/services/audio/rtc_manager.dart - lib/modules/room/voice_room_page.dart - lib/main.dart - lib/ui_kit/widgets/room/** 的现有渲染文件 你的产出要求: - 代码必须可编译到“接口层面合理” - 保持命名稳定,方便线程 B/C 对接 - 在最终回复里列出你定义的关键接口签名和文件列表 ``` --- ### 线程 B Prompt 用途: - 负责 `RoomEffectStage` 和 presentation/layer 承载层 直接投喂: ```text 在遵守“公共约定 prompt”的前提下,执行线程 B 的任务。 你的职责是:搭建语言房特效系统的 presentation/stage/layer 承载层,重点是把房间内特效层级和挂载方式结构化,但不要改旧的 RTM/RTC 入口文件。 你的主要目标: 1. 新增 presentation 模块,优先建议放在: - lib/modules/room_effect/presentation/ - lib/modules/room_effect/presentation/layers/ 2. 建立并实现以下对象或等价对象: - RoomEffectStage - RoomFullscreenEffectLayer - RoomEntranceEffectLayer - RoomGiftTickerEffectLayer - RoomSeatFlightEffectLayer - RoomFloatingEffectLayer 3. 你的核心任务不是重写所有动画,而是先把“承载层和消费接口”搭好。首轮允许通过 bridge 复用现有 renderer。 4. 你需要明确 layer 的顺序和职责,建议至少能表达: - fullscreen 层 - entrance 层 - giftTicker 层 - seatFlight 层 - floating 层 5. 你可以为每个 layer 设计最小消费接口,例如: - 接收 RoomEffectTask 列表 - 接收 lane snapshot - 接收 controller / stream / notifier 6. 你需要尽量让 stage 未来可以挂回 VoiceRoomPage 内部,但本线程先不要大改 voice_room_page.dart。 你的明确写入范围: - 可以新增和修改: - lib/modules/room_effect/presentation/** - 如有必要,可少量只读参考: - lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart - lib/ui_kit/widgets/room/anim/room_entrance_screen.dart - lib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart - lib/ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart 你的禁止写入范围: - 不要修改: - lib/services/audio/rtm_manager.dart - lib/services/audio/rtc_manager.dart - lib/modules/room/voice_room_page.dart - lib/main.dart - 线程 A 负责的新 engine/domain/application 文件 你的产出要求: - 交付可读、清晰的 RoomEffectStage 与 layer 结构 - 接口命名必须对齐公共约定 - 在最终回复中列出: - 你新增的 layer 文件 - 每个 layer 准备承接哪类任务 - 还依赖线程 A 提供哪些 engine 能力 ``` --- ### 线程 C Prompt 用途: - 负责旧链路入口收口与适配层 直接投喂: ```text 在遵守“公共约定 prompt”的前提下,执行线程 C 的任务。 你的职责是:把现有语言房中的特效触发入口统一改成“事件投递”,尽量不碰 presentation 层结构,不大改渲染细节。 你的主要目标: 1. 梳理并改造当前这些文件里的直接特效触发点: - lib/services/audio/rtm_manager.dart - lib/services/audio/rtc_manager.dart - lib/modules/room/voice_room_page.dart - lib/services/gift/gift_system_manager.dart 2. 找出以下直接触发方式并逐步收口: - 直接 SCGiftVapSvgaManager().play(...) - 直接 OverlayManager().addMessage(...) - 直接往独立动画队列入队 - 页面层自己拼装多个动画副作用 3. 你的目标不是立刻删掉所有旧逻辑,而是先把“触发入口”统一改成投递 RoomEffectEvent 或等价事件。 4. 你需要新增或实现适配层,建议命名: - RoomEffectEventAdapter - 或等价 wiring 文件 5. 需要特别注意去重场景: - 本地自己进房 + 群消息回流 - 本地自己送礼 + 房间消息回流 - 幸运礼物广播 + 房间群消息双到达 6. 必须保留当前礼物业务判断口径,不要修改: - 哪些礼物触发全屏 - 哪些礼物触发飘屏 - customAnimationCount 的使用方式 - 幸运礼物 burst 口径 你的明确写入范围: - 可以修改: - lib/services/audio/rtm_manager.dart - lib/services/audio/rtc_manager.dart - lib/modules/room/voice_room_page.dart - lib/services/gift/gift_system_manager.dart - 可以新增: - lib/modules/room_effect/application/room_effect_event_adapter.dart - 或你认为更合适但职责清晰的 adapter/binding 文件 你的禁止写入范围: - 不要修改: - lib/main.dart - lib/modules/room_effect/presentation/** - 线程 B 新增的 stage/layer 文件 - 线程 A 的核心 engine 接口定义 你的产出要求: - 最终让旧入口尽量先统一走事件 - 即便 engine/stage 还未最终接上,也要把触发点收成统一入口 - 在最终回复中列出: - 你改掉了哪些直接触发点 - 每类事件映射成了什么 RoomEffectEvent - 哪些旧逻辑你暂时保留未删 ``` --- ### 线程 D Prompt 用途: - A/B/C 合并后再启动,负责收尾、清理、挂载调整与回归 直接投喂: ```text 在遵守“公共约定 prompt”的前提下,执行线程 D 的任务。 注意:这个线程默认在 A/B/C 已经完成并合并后再启动。你的职责是最终收尾,不是从零搭骨架。 你的主要目标: 1. 把房间专属特效层尽量从根层收回房间内部,重点检查: - lib/main.dart - lib/modules/room/voice_room_page.dart 2. 清理旧双轨逻辑,重点检查: - lib/ui_kit/widgets/room/anim/room_entrance_widget.dart - 旧的 RoomEntranceHelper 残留引用 - 不再需要的直接 play/addMessage 入口 3. 统一房间生命周期清理: - 退房 - 最小化 - 切房 - 房间页面销毁 4. 回归以下重点问题: - 房间退出后礼物图或飞向麦位残留 - 房间飘屏跑到首页 - 进场与全屏特效并发时队列紊乱 - 幸运礼物逻辑回归 5. 你可以少量补充 metrics 或 debug log,但不要无限制新增临时调试代码。 你的明确写入范围: - 可以修改: - lib/main.dart - lib/modules/room/voice_room_page.dart - lib/shared/data_sources/sources/local/floating_screen_manager.dart - lib/shared/tools/sc_room_effect_scheduler.dart - lib/ui_kit/widgets/room/anim/room_entrance_widget.dart - 其它你确认属于“旧链路清理 / 最终挂载调整 / 生命周期回收”的文件 你的禁止写入范围: - 不要推翻线程 A 已经稳定的 engine 核心接口 - 不要重命名线程 B 已经落好的 presentation 核心对象 - 不要重新发明第二套 adapter 或第二套 lane 你的产出要求: - 最终让新链路成为主链路 - 旧链路要么删除,要么明确降级成只读残留且不再实际生效 - 在最终回复中列出: - 你清理了哪些旧链路 - 你改了哪些根层/房间层挂载 - 你验证了哪些关键回归项 - 你认为还剩哪些适合下一轮继续处理 ``` --- ### 推荐投喂顺序 建议你后面实际开线程时,按这个顺序喂: 1. 给 A、B、C 同时发送“公共约定 prompt” 2. 再分别发送各自的线程 prompt 3. 等 A、B、C 产出后完成合并 4. 最后再给 D 发送“公共约定 prompt + 线程 D prompt” ### 推荐合并检查点 在真正合并 A/B/C 前,建议你人工确认一次这几个点: - A 的对象命名和 B/C 预期一致 - B 的 stage/layer 命名没有另起炉灶 - C 没有继续保留太多直接 `play()` 或 `addMessage()` 的新入口 - 三边没有同时改坏 `voice_room_page.dart / rtm_manager.dart / rtc_manager.dart` --- ## 最终建议 最终建议仍然是: - **先重构语言房特效系统** - **不先重构整套语言房** - **先统一入口、统一引擎、统一生命周期** - **尽量保留当前礼物业务逻辑与视觉** 这样做的收益最大,风险也最可控。 如果后续按本文实施,建议第一步从“阶段 0 + 阶段 1”开始,即: - 先建事件模型和指标 - 再把所有直接触发动画的入口统一改成投递事件 一旦这一步完成,后面每条特效链路都可以逐条平滑迁移,不需要再一次性大改整间房。