Files
ft/CodexOutput/upserver/独立复审P2修复方案-2026-09-22.md

87 lines
9.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 独立复审 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 已通过。**