Files
ft/CodexOutput/upserver/最小登录独立复审-2026-09-22.md

107 lines
14 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.
# 最小登录独立复审
日期:2026-09-22。审查者:独立审查子任务 `login_rounds_independent_review`。这是独立源码复审,不是主会话自审的转述。
## 最新结论:修复后独立复审与确定性回归
2026-09-22 同日补充。主会话依据《P2实施裁决-2026-09-22.md》及两次补充修改后,本审查重新读取 **FishingAct、SoundMgr、SoundService、FishingStage 四文件**。下文原始两项 P2 发现保留为修复前历史,不代表它们仍处于未处理状态。
**限定复审通过:两项原 P2 的已证明控制流缺口已修复;主会话随后真实 Unity 发现的 Stage 已 Stop 但尚未销毁窗口,也已补同步 stopping 边界。重新提取生产方法的 58 项本地定向断言均通过,未发现当前增量新的确认阻塞项。** 这是限定代码审查及宿主结论;不替代主会话负责的最新 Unity 回归、真实故障网络或最终渠道包。
### 修复后矩阵
| 证据组 | 结果 | 覆盖 |
|---|---:|---|
| fixed-normal.log / fixed-normal-result.json | 4/4,退出 0 | 原正常 UI、声音初始化与 Release 对照 |
| fixed-regression.log / fixed-regression-result.json | 4/4,退出 0 | 原 4 条安全断言由失败转通过 |
| fixed-extended.log / fixed-extended-result.json | 41/41,退出 0 | GetUI 三等待点、新手三等待点、相机子方法;分别销毁/取消/换容器;声音底层/音频等待退出、失败回滚、新旧初始化交错、重复 Init/Release 与重复地址 |
| fixed-stage.log / fixed-stage-result.json | 9/9,退出 0 | Stage 停止但仍存活且令牌未取消、Stop 尚在等待、旧进度回调、OnDestroy、旧错误过滤和当前错误传播 |
全部位于 `ReviewLifecycleTests/`,修复前退出 1 的日志及 baseline 快照没有覆盖。逐引用断言验证失效资源释放一次、旧完成不释放新代资源;不仅检查总释放次数。最终四生产文件及测试 hash 见 `ReviewLifecycleTests/fixed-source-test-hashes.json`。
### 四文件复审判据
- **FishingAct:** GetUI/NewEnterMap 在入口及各 await 后检查对象、原令牌和原容器;固定原容器解析服务;OnCameraSpeedEvent 的内部等待也有检查。当前成功流程保留,不主动销毁 UIManager 拥有的 UI,不改奖励/补单判定。
- **SoundService:** 两个句柄由本次 Init 的局部变量持有;Release 先失效版本,仅清理已移交成员。当前失败回滚、局部 finally 及迟到完成互不重复释放;旧代失败不会调用新代 Release。建池/订阅失败后清理正常;正常加载失败仍传播。
- **SoundMgr:** 全部音频引用由局部 opMap 追踪,重复地址只加载一次;未移交时 finally 清理本次句柄/订阅,当前代失败释放底层服务,旧代失败不释放新代。正常完成才移交成员;重复成功 Init 会先释放原成员。每个等待恢复后原容器/版本检查早于新订阅及音量写入。
- **FishingStage:** Stop 首行置 stopping,早于容器注销与成员清空;Start 不把它清回 false,OnDestroy 也置位。新增条件覆盖既有 await/进度/旧初始化异常过滤,避免把已停止实例当仍可初始化的对象。当前有效启动异常仍抛出;没有新建 CompositeDisposable 让旧阶段继续启动。
### 仍需如实保留的证据边界
1. GetUI/NewEnterMap 的 async void 仅在测试提取时转 Task,以观察完成与异常;真实 Unity 仍由主会话复测。底层 UIManager 自身的加载/销毁所有权未改,不能据外层 guard 宣称全部 UI 异步流程安全。
2. SoundMgr 的声音服务调用由替身控制;SoundService 使用自身实际提取方法单独验证。两层引擎集成、混音器销毁和实际 Addressables 行为需 Unity 证据。失败资源/订阅/音量/建池通过替身注入,并非真实服务器故障。
3. Stage 测试提取 Start 前缀至声音初始化后、完整 Stop/OnDestroy 和实际 base.StartAsync catch 片段;没有提取全部首页服务初始化链。真实 `p2-progress-102-errors.json` 由主会话记录,本审查未运行 Unity,也没有将其失败日志重写为通过。
4. 加载失败在已失效旧任务中仍可能传播,测试明确验证旧代失败不污染新代资源;没有把所有旧任务错误吞掉。当前结论是此轮范围内资源与续体边界,不是全项目异常收敛。
---
以下为修复前独立发现与原始审查记录。
## 结论
**本次未确认前轮五处、第二轮四处补丁引入新的 P0/P1 回归;发现两项现存 P2 生命周期缺口,需定向复现和裁决后才能关闭相关退出验收。** 不建议把「三轮快速换号错误为零」提升为所有首次初始化退出窗口均已覆盖。
两项问题在旧代码中已存在,本轮保护没有覆盖它们;不是要求回退已有补丁。本报告不改业务代码、不运行 Unity、不操作正式网络,也不启动 KV、俱乐部或 SDK 删除。打包及真机仍按用户要求留到最后。
## 审查依据与范围
- 已读《当前阶段范围裁决-仅最小登录.md》《Unity重登缺陷-方案与评审.md》《Unity重登修复-限定代码审查.md》《最小登录收尾-第二轮验收方案.md》《登录提前退出声音释放-修复方案与评审.md》《最小登录第二轮-限定代码审查.md》《最小登录收尾-第二轮验收结果.md》。
- 基线提交 `f40a816cc`。逐项检查当前工作区九文件 diff,并阅读资源所有权、初始化、销毁及直接调用方。PFLoginAct/QuitAct 的完整 diff 含更早的最小登录改动,未全部误认作前轮新增。
- 九文件当前 SHA256 均与 `uloop-repair-source-hashes.json`、`acceptance-round2-source-hashes.json` 对应记录一致。行号以下列审查时源码为准。
- 为确认隐式取消机制,仅通过 Unity.Cecil 只读读取 `Client/Assets/CoreLib/DebugLib/asap.core.dll`;没有改 DLL。`Awaiters.SecondsRealtime/NextFrame` 经 `IEnumeratorAwaitExtensions` 在 `UnityAsyncAwaitUtil.AsyncCoroutineRunner.Instance` 上启动协程;该 Runner 在 Awake 中调用 DontDestroyOnLoad,等待与 FishingAct/FishingStage 的销毁没有绑定。
## 发现 1:P2 — 首次首页初始化的 async void 续体仍会越过退出边界
**位置:** `Client/Assets/Scripts/Stages/FishingAct.cs:346`,尤其 351–359、366–376;入口为同文件 268 行 `GetUI()`。
StartAsync 在地图加载后新增的生命周期检查有效,但随后启动了独立的 `async void GetUI()`。该方法等待 `RestartShowUI`、`ReleaseReplenishment` 和固定 0.5 秒后,不检查对象、原登录令牌或原容器,直接从当前 `GContext.container` 获取 `FishingEventData`,并继续操作旧 `fishingBehavior` 和首页。`StopAsync` 会销毁 `_mapObj`、注销 FishingData;`OnDestroy` 只处理景深,没有取消该方法。销毁 FishingAct 不会停止全局 Runner 中的等待。
因此,在首次首页初始化的等待窗口退出,旧续体仍可在 QuitStage 或下一容器运行。若当前容器已经重建,会执行新容器的 `ShowRankTierChange`;若旧地图已销毁,后续 `fishingBehavior.cameraAnimator` 等访问可抛出 MissingReference/NullReference。FishingStage 在 `await base.StartAsync()` 外的异常过滤不能捕获另一个 `async void` 的异常。
**归属:** 现存初始化链缺口,不是本轮新增。此前真实测试只证明采样时序通过;本审查尚未实际复现此窗口。
**最小复现:** 使用现有 101 正常进入首页,在 GetUI 已开始、0.5 秒等待尚未结束时走生产 QuitStage。确认等待恢复后没有旧对象访问、没有当前容器服务调用。定向宿主可控制 UI/延时任务完成顺序,先证实旧代码在失效后继续执行;引擎实例销毁仍需 Unity 验证。无需抛竿、购买、补单造数或改存档。
**建议最小边界:** FishingAct.cs 的首进首页初始化入口和 GetUI。捕获原生命周期/容器,在每个 await 后先检查对象与原会话,再访问服务或 UI;旧流程只清理本次拥有的迟到资源。若底层 UI await 自身抛错,区分已销毁/已取消的旧流程和仍有效的当前错误,不能无条件吞异常。`NewEnterMap`(299–315 行)是同一首登链的新手分支,也有等待后访问旧对象;现有 101/102 等级无法覆盖,需单列证据,不要修改账号等级制造测试。
**验收条件:** 正常首页仍显示;在各等待点退出后无旧 UI/当前容器副作用;有效会话的真实加载失败仍可见。不能用延长换号等待替代此项。
## 发现 2:P2 — 声音初始化未完成时 Release 不能阻止迟到初始化重新注册
**位置:** `Client/Assets/Scripts/Core/SoundMgr.cs:233`(233–248),关联 `:201`、`:758`;`Client/Assets/Scripts/Sound/SoundService.cs:143`、`:159`、`:177`;新增外层保护位于 `Client/Assets/Scripts/Stages/FishingStage.cs:172`。
SoundMgr.Init 在 `soundService.InitAsync()` 后,把 residentFish 的加载句柄仅保存在局部 opMap,并逐帧等待。此时 Release 只能处理已经赋值的 residentClip/disposables;它们尚未赋值时会跳过,随后释放 SoundService。旧 Init 没有代际或取消检查,资源完成后仍会赋 residentClip、创建新的 CompositeDisposable、注册全局声音事件,并通过当前 GContext 设置已释放 SoundService 的音量。
SoundMgr/SoundService 是容器注册的普通 C# 单例,不是销毁时自动停止协程的 MonoBehaviour。FishingStage 只在整个 Init 返回后检查令牌,这晚于上述副作用;其进度回调过滤也不会取消 Init。SoundService.InitAsync 自身两个 Addressables await 后同样无 Release 边界,在其内部退出还可能出现释放后继续创建声音池/订阅的相邻窗口。
**影响:** 退出后旧声音资源或事件订阅恢复;可能操作已释放 mixer/对象池,或订阅到新的全局会话。当前 Release 的可空/IsValid 补丁解决了未初始化退出的空引用,但不能代表进行中初始化也安全。
**归属:** 原有并发窗口,声音修复方案已明确当时未覆盖;不是本轮补丁新增。源码路径已经确认,本审查未运行资源时序复现。
**最小复现:** 先令 SoundService 正常初始化,把 SoundMgr residentFish 至少一个资源完成点保持等待;调用真实 Release,再完成该资源;检查旧 Init 不得重新注册事件、设置音量或保留句柄。另覆盖 SoundService 的 mixer/prefab 两个等待点分别 Release。先做受控定向宿主,再于 Unity 验证实际 Addressables 所有权;不必改变正式 API、全局网络或存档。
**建议最小边界:** `SoundMgr.cs` 的 Init/Release 和 `SoundService.cs` 的 InitAsync/Release(如需令牌参数,显式列出接口及唯一调用方差异)。由每次初始化持有独立生命周期/代际,Release 先使其失效;每个 await 恢复后先检查,释放仅属于本次的迟到句柄,失效后不再建池或订阅。资源所有权应避免 Release 与迟到续体双重释放;不改音量配置、音效内容或播放规则。
**验收条件:** 正常初始化、完全未初始化释放、两级初始化各等待点退出、连续两次释放,以及退出后新实例初始化;旧实例资源释放恰好一次,旧订阅数量归零,新实例音效正常。
## 其余增量核对
| 文件 | 独立核对结论 |
|---|---|
| PFLoginAct.cs | 输入模式重试重新等待输入;submitted 在 await 前封闭重复点击;取消注销输入监听,返回后检查原 token。未找到本次输入重试增量的确认缺陷。 |
| ActivitySentryManager.cs | 容器替换时显式/懒初始化清理两份类型缓存,同容器保持重复注册保护。GetTypeKey 本身不初始化,但正常 FishingStage.InitManager 先完整注册;未据此认定本次有可触发回归。 |
| QuitAct.cs | Timer.CancelAllRegisteredTimers 后 Destroy;前置注销和异步 RT 释放顺序保留。未找到新增 Timer 调用的确认缺陷。 |
| PlayFabMgr.cs | 未授权检查在出队/交换前;本地 Save 不清空数据。它只保留本地缓存,不保证待同步数据之后一定上传;现有 60 秒比较和失败补传仍是已声明限制,不作为本次新增缺陷。 |
| HomePanel.cs | LastShowPanel 在延时后检查对象与原容器,未复用新容器服务。此保护不覆盖发现 1 的外层 GetUI。 |
| SoundService.cs | Release 对空字典、默认/重复句柄、空订阅的增量有效。进行中 Init 的问题见发现 2。 |
| FishingAct.cs | 地图资源先放局部变量,旧会话释放本次返回资源;StartAsync 直接 await 处有检查。分离的 async void 首屏后续见发现 1。 |
| FishingBehaviorB.cs | 已销毁对象的手/饵实例、杆 prefab 迟到后走对应释放;SetIKTarget 容忍未建杆。未确认这四处增量带来新资源所有权错误。 |
| FishingStage.cs | 正常错误不进入已销毁/取消过滤;首登外层检查有效。其保护不取消内部服务副作用,也捕获不到独立 async void,不能推导出两个发现已经被覆盖。 |
## 测试证据局限与交接
- 已读取原始最终错误数 0、编译 0 错误/271 警告及快速 101→102→101 执行日志;这些是真实已完成的历史证据,本审查没有重跑或否定它们。
- SoundExitTests 生成器仅提取 Release,没有 Init/Release 交错;MapExitTests 只取地图加载前半段、SetIKTarget 和 Stage try/catch 片段,没有 GetUI/SoundMgr 的等待链。因此现有定向通过无法排除上述两项。
- 两项发现均需主会话先落盘方案裁决,再按用户「发现问题再编码修复」要求处理。没有授权扩大玩法、改服地址或做存档迁移。
- 当前裁决:**独立审查已完成,有两项现存生命周期缺口待验证/处理;不是整体最小登录无条件验收通过。** HTTP 故障、真实断线重连、必要玩法和最终真机的证据由主会话分别记录,本审查不替代这些验收。