45 KiB
语言房重构方案
文档目的
本文只记录一套可执行的重构方案,本轮不改业务代码。
本次建议的重构范围不是“整套语言房全部推倒重写”,而是优先重构语言房特效子系统,重点解决以下问题:
- 礼物特效、进场、飞向麦位、房间飘屏并发时主线程抢占严重
- 特效触发源分散,队列和调度分散,后续越优化越难维护
- 房间内特效偶发残留到首页/其它页面,生命周期边界不清晰
- 已做过多轮局部优化,但整体架构仍然偏重,继续打补丁收益会越来越低
本方案的核心原则是:
- 尽可能保留当前礼物业务逻辑、素材、阈值和视觉表现
- 先重构特效系统,不一次性推倒
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 仍保留
根因判断
当前卡顿的根因更偏向结构性问题,而不是单个控件或单次绘制的问题:
-
多源头直接驱动 UI
RTM/RTC/RoomPage/GiftSystem都可能直接触发动画。 -
多条高频动画链路并发
全屏VAP/SVGA、顶部礼物条、进场 banner、飞向麦位、房间飘屏可能同时争抢主线程。 -
房间特效不是一个完整子系统
现在只有局部管理器,没有一个真正拥有“总入口、总队列、总预算、总生命周期”的 runtime。 -
业务状态更新和特效播放距离太近
房间麦位刷新、在线用户刷新、消息列表更新,会和特效播放共享同一个页面状态环境。 -
生命周期边界偏松
房间退出后,根层 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 表达方式 | 保留视觉,不一定保留旧实现 |
建议保留但从硬编码改为配置化的参数
- 礼物播报槽位数
- 礼物播报待播上限
- 进场队列上限
- 飞向麦位单会话上限
- 高成本特效队列上限
- 飘屏丢弃阈值
- 各类优先级与独占关系
目标架构
架构总览
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/。
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,至少包含以下信息:
eventIdroomIdeventTypesourcecreatedAtprioritydedupKeymergeKeyuserIdtargetUserIdgiftIdpayloadrequiresHeavyRendererroomScoped
推荐事件类型
roomUserJoinroomMountEntrancegiftTickergiftFullscreengiftSeatFlightgiftFloatingluckyGiftMilestoneluckyGiftRewardTickerluckyGiftBurstrocketFloatingredPacketFloatinggameFloating
为什么要有标准化事件层
- 后续所有去重和合并都建立在统一事件模型上
- 可把“同一礼物事件触发 3 种视觉表现”拆成 3 个子任务,而不是让 RTM 直接调用 3 个 UI 系统
- 可支持本地自发事件和远端回流事件的统一去重
目标任务模型
事件进入引擎后,不直接播放,而是先变成 RoomEffectTask。
一个 task 建议至少包含:
taskIdlanepriorityenqueueAtdeadlineexclusivemergeablemergeKeydropPolicypayloadonStartonCompleteonCancel
推荐的 lane 划分
| Lane | 作用 | 特点 |
|---|---|---|
fullscreen |
全屏礼物、幸运礼物 burst、进场坐骑等高成本特效 | 可独占,优先级最高 |
entrance |
房间进场 banner | 串行,可合并,可被延后 |
giftTicker |
顶部礼物播报条 | 维持 4 槽位模型 |
seatFlight |
飞向麦位 | 可批量、可会话级去重 |
floating |
房间飘屏、幸运礼物飘屏、火箭、红包、游戏 | 可按消息类型和房间范围过滤 |
页面挂载方式调整
当前问题
当前房间特效有一部分挂在根层 MaterialApp builder,例如:
- 全屏
VapPlusSvgaPlayer RoomGiftSeatFlightOverlay
这意味着:
- 特效承载层不完全属于房间页面
- 房间退出后需要额外兜底清理
- 根层 overlay 容易被误复用到非房间页面
目标方式
推荐新增 RoomEffectStage,只挂在语言房页面内:
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/rasterP95 fullscreen lane平均等待时长entrance/floating/seatFlightbacklog 峰值- 被丢弃任务数量和占比
- 同事件重复触发数
- 房间退出后残留特效数
目标方向
- 房间高压场景下明显减少整页卡顿和掉帧
- 不再出现房间特效溢出到首页
- 不再依赖在多个管理器里各自补清理逻辑
- 新增一个特效类型时,只需要走“标准化事件 -> 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,一边让多个线程各自发明自己的事件模型
- 不建议一开始就多个线程同时删旧链路
并行原则
-
先统一接口,再并行开发
至少先约定好以下对象名和职责:RoomEffectEventRoomEffectTaskRoomEffectLaneRoomEffectEngineRoomEffectStageRoomEffectEventAdapter
-
新文件优先并行,旧文件尽量收口到单一线程
新增模块最适合多线程拆分;旧有入口文件应尽量减少多人同时修改。 -
先搭骨架,再接现有链路
先让 engine 与 stage 的框架稳定,再开始把RTM/RTC/VoiceRoomPage接上去。 -
收尾一定要串行
旧链路删除、层级调整、房间生命周期回收、回归修 bug,这些必须收口到最后一个线程统一处理。
推荐线程划分
线程 A:Engine 基础层
定位:
- 负责“语言房特效引擎”的核心模型与调度骨架
推荐负责范围:
lib/modules/room_effect/domain/lib/modules/room_effect/application/lib/modules/room_effect/infrastructure/中和调度、预热、生命周期守卫相关的文件
推荐文件所有权:
room_effect_event.dartroom_effect_task.dartroom_effect_lane.dartroom_effect_priority.dartroom_effect_engine.dartroom_effect_policy.dartroom_effect_deduplicator.dartroom_effect_merger.dartroom_effect_metrics.dartroom_effect_asset_preloader.dartroom_effect_lifecycle_guard.dart
本线程不建议碰:
rtm_manager.dartrtc_manager.dartvoice_room_page.dart- 现有具体动画 widget 的渲染实现
本线程交付物:
- 可编译的 engine/domain/application 基础骨架
- lane、task、event、policy 的统一接口
- 基本的 backlog、优先级、去重、合并接口
线程 B:Stage 与渲染承载层
定位:
- 负责“语言房特效舞台”和各 layer 的承载结构
推荐负责范围:
lib/modules/room_effect/presentation/
推荐文件所有权:
room_effect_stage.dartlayers/room_fullscreen_effect_layer.dartlayers/room_entrance_effect_layer.dartlayers/room_gift_ticker_layer.dartlayers/room_seat_flight_layer.dartlayers/room_floating_effect_layer.dart- 如果需要,可新增 presentation 侧 controller 或 snapshot 文件
本线程可复用但不建议大改的旧文件:
lib/ui_kit/widgets/room/anim/l_gift_animal_view.dartlib/ui_kit/widgets/room/anim/room_entrance_screen.dartlib/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dartlib/ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart
建议策略:
- 先做 layer 容器和接口桥接,不先重写底层动画实现
- 首轮允许通过 bridge 调现有 renderer
本线程不建议碰:
rtm_manager.dartrtc_manager.dartmain.dart
本线程交付物:
RoomEffectStage- 各特效 layer 的挂载顺序和层级
- 渲染层与 engine 的消费接口
线程 C:旧链路入口收口与适配层
定位:
- 负责把现有
RTM/RTC/VoiceRoomPage/GiftSystem的特效触发点统一改成事件投递
推荐负责范围:
lib/services/audio/rtm_manager.dartlib/services/audio/rtc_manager.dartlib/modules/room/voice_room_page.dartlib/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.dartlib/modules/room/voice_room_page.dartlib/shared/data_sources/sources/local/floating_screen_manager.dartlib/shared/tools/sc_room_effect_scheduler.dartlib/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.dartlib/services/audio/rtc_manager.dartlib/modules/room/voice_room_page.dartlib/main.dart
这些文件建议默认归线程 C 或线程 D 所有。
第 3 步:先合 A,再合 B,再合 C
推荐合并顺序:
-
先合线程 A
先把 engine 和模型定住,避免后续接口继续漂。 -
再合线程 B
让 stage 和 layer 按 A 的接口接上。 -
再合线程 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.dartvoice_room_page.dartrtm_manager.dartrtc_manager.dart
这些文件很容易形成高冲突区,激进并行最后未必更快。
最终建议的并行执行口径
如果你后面准备“分别跑”多个线程,建议直接按下面口径执行:
- 线程 A:只做 engine/domain/application 基础层
- 线程 B:只做 stage/presentation/layer 承载层
- 线程 C:只做旧入口收口和事件适配
- 线程 D:最后统一清理旧链路、改挂载、跑回归
这是这次语言房特效系统重构里,最稳、最不容易互相打架的一种并行拆法。
可直接投喂 Codex 的执行 Prompts
下面这组 prompt 是给你后面开多个 Codex 线程时直接复制使用的。
推荐使用方式:
- 先把“公共约定 prompt”复制给每个线程
- 再把对应线程的专属 prompt 追加给该线程
- 线程 D 不要一开始就跑,等 A/B/C 合并后再启动
公共约定 Prompt
建议把下面这段先发给每一个线程,作为共同上下文。
你现在在 /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 基础层
直接投喂:
在遵守“公共约定 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 承载层
直接投喂:
在遵守“公共约定 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
用途:
- 负责旧链路入口收口与适配层
直接投喂:
在遵守“公共约定 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 合并后再启动,负责收尾、清理、挂载调整与回归
直接投喂:
在遵守“公共约定 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
你的产出要求:
- 最终让新链路成为主链路
- 旧链路要么删除,要么明确降级成只读残留且不再实际生效
- 在最终回复中列出:
- 你清理了哪些旧链路
- 你改了哪些根层/房间层挂载
- 你验证了哪些关键回归项
- 你认为还剩哪些适合下一轮继续处理
推荐投喂顺序
建议你后面实际开线程时,按这个顺序喂:
- 给 A、B、C 同时发送“公共约定 prompt”
- 再分别发送各自的线程 prompt
- 等 A、B、C 产出后完成合并
- 最后再给 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”开始,即:
- 先建事件模型和指标
- 再把所有直接触发动画的入口统一改成投递事件
一旦这一步完成,后面每条特效链路都可以逐条平滑迁移,不需要再一次性大改整间房。