87 lines
9.1 KiB
Markdown
87 lines
9.1 KiB
Markdown
# 独立复审 P2 修复方案与实施前评审建议
|
||
|
||
日期:2026-09-22。独立审查任务编制;生产代码尚未修改。本文件供主会话完整评审并明确实施裁决。范围仍为最小登录首次场景初始化/退出,正式 API 固定 http://s101.local:18080/;不打包,不推进 KV、俱乐部或 SDK 删除。
|
||
|
||
## 1. 确定性缺陷证据
|
||
|
||
已建立 `ReviewLifecycleTests` 本地宿主,直接提取实际 GetUI、SoundMgr.Init/Release、SoundService.InitAsync/Release。仅为等待测试将 GetUI 的 async void 改成 public async Task;其余方法体不改。提取来源/方法 hash 见 extraction.json;修复前快照保存在 baseline/。
|
||
|
||
| 实际方法、控制时序 | 正常对照 | 退出安全断言结果 |
|
||
|---|---|---|
|
||
| GetUI 的 0.5 秒等待期间标记原对象销毁并替换容器,再完成等待 | RankCalls=1、HomeCalls=1、CameraWrites=1,正常流程通过 | 同样三个副作用仍各发生 1 次,安全断言失败 |
|
||
| SoundMgr.Init 音频等待时调用真实 Release,再完成音频与 NextFrame | 初始化 10 个订阅,Release 后 0;无释放后音量写入 | 重新注册 10 个订阅,释放后音量写入 3 次,保留 2 个 clip,安全断言失败 |
|
||
| SoundService.InitAsync 等待 mixer 时调用真实 Release,再完成加载 | 正常建池 1 次,释放后订阅 0、句柄释放 2 | 仍建池 1 次、订阅 1 个,并保留迟到 prefab 句柄,安全断言失败 |
|
||
| SoundService.InitAsync 等待 prefab 时 Release,再完成加载 | 同上 | 仍建池 1 次、订阅 1 个,安全断言失败 |
|
||
|
||
`normal.log` 退出码 0;`regression-failing.log` 退出码 **1**,4 项安全断言失败。不是将预期失败算作验收通过。没有网络、真实存档、Unity 或真机操作。
|
||
|
||
**证据层级:** 已确定性复现真实方法的控制流缺口;资源与引擎为替身。真实 Addressables 可能先抛已释放句柄/对象异常,因此不把宿主副作用计数当作 Unity 实际结果。修复后需实际 Unity 编译及退出/重登回归。
|
||
|
||
## 2. 最小生产文件边界
|
||
|
||
仅三文件:
|
||
|
||
1. `Client/Assets/Scripts/Stages/FishingAct.cs`:GetUI、NewEnterMap,以及必要的私有生命周期检查辅助;不改奖励、补单、摄像机动画规则。
|
||
2. `Client/Assets/Scripts/Core/SoundMgr.cs`:Init、Release、私有初始化版本字段及必要资源清理辅助;不改事件类型、音量选项或播放逻辑。
|
||
3. `Client/Assets/Scripts/Sound/SoundService.cs`:InitAsync、Release、私有初始化版本字段;保留 ISoundService 接口与现有方法签名。
|
||
|
||
不修改框架 DLL、Addressables 包、全局 Runner、其它玩法异步方法或服务器。外围既有差异原样保留。测试及评审证据只在 CodexOutput/upserver。
|
||
|
||
## 3. P2-1:首进首页续体
|
||
|
||
### 实施方案
|
||
|
||
- GetUI / NewEnterMap 开始先拒绝已销毁对象,捕获原 GContext.container 和原 IUserService.LoginLifetime。
|
||
- 每个 await 返回之后、调用新服务/UI/相机之前,检查 `this == null`、原令牌取消、容器引用变化;任一命中则停止旧流程。
|
||
- 方法内服务解析统一使用捕获容器;这只固定原会话归属,不将旧服务带入新会话。
|
||
- NewEnterMap 的新手相机等待、OnCameraSpeedEvent 等待、RestartShowUI 等待均需边界检查。正常 101/102 不进入新手分支,宿主需直接控制此分支,不修改正式账号等级来测试。
|
||
- 外层检查无法阻止 await 内部异步方法自身已经产生的副作用:只在证据证明需要时保护直接子方法的同一首进初始化调用,不能假设外层检查等于子方法自动取消。尤其 OnCameraSpeedEvent 自身等待后写场景,需要明确捕获/检查,或将其首登所需等待拆到受控方法内;若增加该方法,先在最终裁决列明。
|
||
- 旧流程失败需要处理时,仅允许对象已销毁/原令牌取消/原容器已替换的过滤;当前有效流程异常仍应保留原日志可见性,不做无条件 catch。方法可保留原签名,避免更改所有调用点;若使用 Task 核心加 async void 薄入口,应记录新增方法。
|
||
|
||
### 所有权
|
||
|
||
UIManager 管理的 HomePanel 不由 FishingAct 无条件 Release/Destroy,避免销毁新会话 UI。返回时只停止旧控制流;如出现迟到 UI 实例泄漏,依据 UIManager 的实际所有权另行确认,不能用 ReleaseInstance 猜测处理。这里不新增补单请求,不修改原有补单判定。
|
||
|
||
## 4. P2-2:声音初始化与释放交错
|
||
|
||
### SoundService
|
||
|
||
- 新增私有初始化版本(例如 `_initializationVersion`),InitAsync 捕获本次版本;Release 的第一步递增版本,使进行中的 Init 失效。
|
||
- mixer/prefab **先只由 Init 局部句柄拥有**,放在 try/finally 中。等待恢复后先比版本,再使用资源;旧版本 return 的 finally 释放仍由局部持有的有效句柄。
|
||
- Release 只处理已经移交成员的句柄,不释放另一协程仍持有的局部句柄;避免同一资源被 Release 与旧 Init 双重释放。
|
||
- 只有两种资源加载成功且仍属当前版本,才完成成员赋值、建池与事件订阅;成功移交后清空局部所有权标记,使 finally 不再释放成员拥有的句柄。
|
||
- 建池/订阅若可能抛异常,移交与回滚需要明确:失败不得留下只有一半成员资源/事件。正常当前错误继续传播;仅已失效的旧加载失败可按受控退出记录。不能只写 await 后检查而让 await 抛错绕过处理。
|
||
- 正常初始化完成后重复 Release 仍幂等。若允许同一实例再次 Init,开始新一代前必须释放旧成员所有权,或直接复用已成功状态;禁止新一代直接覆盖有效句柄导致旧资源丢失。容器重建通常创建新实例,但实现和定向测试必须避免隐含双持有。
|
||
|
||
### SoundMgr
|
||
|
||
- 增加独立初始化版本,Release 最先使版本失效;Init 同时捕获原容器。底层 InitAsync 返回、每次 NextFrame 后、事件订阅/音量设置前校验本次版本和原容器。
|
||
- 局部 opMap 持有本次所有音频句柄。当前 residentFish 源列表包含重复 `audio_fishing_waiting`;应按地址只加载一次(不改变按地址取得 clip 的结果),避免覆盖句柄而丢失引用。此处仅为完成资源所有权,不改播放列表/音效规则。
|
||
- 音频等待放入 try/finally。失效/失败时只释放本次局部有效句柄;当前成功时将结果所有权移交 residentClip,finally 不再释放已移交的引用,继续沿用 Release 对对象的释放方式。
|
||
- 注册事件和设置音量只能在仍有效的当前初始化内发生;若中途失败,要撤销本次已经创建的订阅并释放拥有的资源,不留下半初始化。正常失败不得被无条件忽略。
|
||
- 版本守卫同时覆盖底层声音服务返回和资源加载循环,不能只过滤 progress 回调。正常完成后旧容器 Release 清理成员;迟到局部资源由局部 finally 收尾。
|
||
|
||
### 资源归属表
|
||
|
||
| 时刻 | SoundService mixer/prefab | SoundMgr 音频 |
|
||
|---|---|---|
|
||
| await 进行中 | 本次 Init 的局部句柄 | 本次 Init 的局部 opMap |
|
||
| 退出/版本失效 | Release 使版本失效;局部 finally 在完成/失败后释放 | 同左,原成员 Release 不接管旧 opMap |
|
||
| 当前版本完整成功 | 成员句柄拥有,局部所有权清空 | residentClip 拥有,局部所有权清空 |
|
||
| 成功后 Release | 有效成员释放一次、清空 | 成员 clip 释放一次、清空、事件 Dispose |
|
||
|
||
**独立方案评估:** 主会话提出的「局部句柄 try/finally,完整成功后移交;Release 先失效版本」方向适合这两个已复现窗口,不需要接口改动。以上移交/失败回滚/重复地址条件必须纳入完整实施,才能断言资源恰好释放一次。
|
||
|
||
## 5. 修复验证矩阵
|
||
|
||
1. 重新提取生产方法,不修改 baseline/。现有四项安全断言从失败转为通过,四项正常对照继续通过。
|
||
2. GetUI 每个等待点的销毁/取消/换容器;NewEnterMap 的等待退出;当前正常 UI 操作保持。依赖替身补 Unity 对象等号语义,不用替身实现待测业务保护。
|
||
3. SoundService mixer/prefab 等待分别 Release;SoundMgr 底层 Init 等待、音频等待分别 Release;退出后无新池、无旧订阅、无音量写入。
|
||
4. 资源成功/失败、重复 Release、旧初始化晚于新一代完成、重复音频地址:每次获得引用恰好移交或释放一次,不释放新一代句柄。需要替身记录每个引用,而不只计总数。
|
||
5. Unity CLI Loop 编译,再执行正常登录/快速退出重登/实际 Hub Test;记录 Error 与新增受控取消 Warning。无需为这次修复重测所有暂停模块。
|
||
6. 新补丁独立复审按上述三文件限定增量,不继承此前静态通过结论。实际未命中的资源窗口仍明确是宿主覆盖。
|
||
|
||
## 6. 开发裁决状态
|
||
|
||
独立审查建议:上述边界方案可实施,先由主会话确认完整裁决并记录任何方法边界补充;未完成裁决前不改生产代码。**本文件只给方案与独立评审建议,不冒充主会话已经执行修复或 Unity 已通过。**
|