14 KiB
最小登录独立复审
日期: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 让旧阶段继续启动。
仍需如实保留的证据边界
- GetUI/NewEnterMap 的 async void 仅在测试提取时转 Task,以观察完成与异常;真实 Unity 仍由主会话复测。底层 UIManager 自身的加载/销毁所有权未改,不能据外层 guard 宣称全部 UI 异步流程安全。
- SoundMgr 的声音服务调用由替身控制;SoundService 使用自身实际提取方法单独验证。两层引擎集成、混音器销毁和实际 Addressables 行为需 Unity 证据。失败资源/订阅/音量/建池通过替身注入,并非真实服务器故障。
- Stage 测试提取 Start 前缀至声音初始化后、完整 Stop/OnDestroy 和实际 base.StartAsync catch 片段;没有提取全部首页服务初始化链。真实
p2-progress-102-errors.json由主会话记录,本审查未运行 Unity,也没有将其失败日志重写为通过。 - 加载失败在已失效旧任务中仍可能传播,测试明确验证旧代失败不污染新代资源;没有把所有旧任务错误吞掉。当前结论是此轮范围内资源与续体边界,不是全项目异常收敛。
以下为修复前独立发现与原始审查记录。
结论
本次未确认前轮五处、第二轮四处补丁引入新的 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 故障、真实断线重连、必要玩法和最终真机的证据由主会话分别记录,本审查不替代这些验收。