Files
FlowScope/docs/reviews/p2-precheck-fix-review.md
2026-05-21 15:05:56 +08:00

8.7 KiB
Raw Blame History

FlowScope P2 前整改复审意见

日期2026-05-21
审查对象:558bc91 完成 P2 前架构整改
审查范围Container 生命周期、GameFlow/Feature 生命周期、MainMenuP0 Bootstrap、UIPreloadService、ResourceService 取消语义、相关文档与测试。
审查结论:本轮整改方向正确,已收口上一轮最关键的 Container/Feature 重入问题;但仍有两个 P1 级风险需要在进入 P2 前修正。


本轮已确认收口

1. Container 生命周期语义已明显改善

当前实现新增并区分:

  • RegisterSingletonFactory
  • RegisterScoped
  • RegisterTransient

RegisterFactory 保留为旧式 lazy singleton 兼容入口,文档中已提示不要用于 Feature 或临时业务对象。

相关文件:

  • My project/Assets/FlowScope/Runtime/Container/Container.cs
  • My project/Assets/FlowScope/Runtime/Container/ContainerRegistration.cs
  • My project/Assets/FlowScope/Tests/EditMode/Container/ContainerTests.cs

新增测试覆盖:

  • singleton 由注册 scope 持有。
  • transient 每次 resolve 创建新实例,并由请求 scope 释放。
  • scoped 在同一请求 scope 内复用,不同 scope 分离。

评价:上一轮指出的“RegisterFactory 语义误导 + Feature 复用已 Dispose 实例”问题已经被大幅降低。


2. Feature 重入问题已补测试

相关文件:

  • My project/Assets/FlowScope/Tests/EditMode/Flow/GameFlowTests.cs
  • My project/Assets/FlowScope/Samples/MainMenuP0/Scripts/GameBootstrap.cs

已新增测试:

  • SwitchToAsync_WhenReturningToFeatureType_CreatesNewFeatureInstance

样例中 MainMenuFeature 已改为:

_root.RegisterTransient(_ => new MainMenuFeature(screenNavigator, _playerData));

评价:A -> B -> A 场景已经有明确保护,符合 P2 前最低要求。


3. Bootstrap 启动入口可观察性已改善

相关文件:

  • My project/Assets/FlowScope/Samples/MainMenuP0/Scripts/GameBootstrap.cs
  • My project/Assets/FlowScope/Tests/PlayMode/Samples/MainMenuP0Tests.cs

已从 async void Awake() 直接 await 改为:

  • Awake() 创建 lifetime token。
  • StartupTask = RunStartupWithLoggingAsync(...)
  • PlayMode 测试覆盖 Awake_StartsThroughObservableStartupTask

评价:启动异常现在可以通过 StartupTask 被观察,样例不再鼓励直接在 Awake 中 await。


4. ResourceService 共享加载取消边界已有补测

相关文件:

  • My project/Assets/FlowScope/Tests/EditMode/Resources/ResourceServiceBackendTests.cs

新增测试覆盖:

  • 一个共享等待者取消,另一个等待者仍能拿到 handle。
  • 所有共享等待者取消后,底层完成时释放 backend asset不进入 completed cache。

评价:资源层的“调用者取消不取消共享底层加载”语义更清楚了。


仍需修正的问题

P1. GameBootstrap.ShutdownAsync 失败后会永久跳过后续清理

风险等级:高
建议:进入 P2 前修正。

证据:

文件:My project/Assets/FlowScope/Samples/MainMenuP0/Scripts/GameBootstrap.cs

当前流程:

if (_shutdownStarted)
{
    return;
}

_shutdownStarted = true;

await _saveService.SaveAsync(...);
await _gameFlow.ShutdownAsync(...);
_preloadService?.ReleaseAll();
_shutdown = true;

问题:

  • _shutdownStarted = true 在保存、GameFlow 关闭和预加载释放之前设置。
  • 如果 SaveAsync 抛异常,GameFlow.ShutdownAsync 不会执行。
  • 如果 GameFlow.ShutdownAsync 抛异常,_preloadService.ReleaseAll() 不会执行。
  • 因为 _shutdownStarted 已经是 true之后再次调用 ShutdownAsync 会直接 return。
  • 结果是 _shutdown 不会置 true预加载资源和 runtime 对象可能残留。

建议修法:

  1. 将“正在关闭”和“已完成关闭”拆成不同状态。
  2. 关闭流程应使用 best-effort / finally
    • 尽量保存。
    • 尽量关闭 GameFlow。
    • 无论前面是否失败,都释放预加载资源。
    • 最后根据结果记录异常或聚合异常。
  3. 如果第一次关闭失败,应允许再次尝试未完成的清理,或者至少确保清理已经执行过。

建议补测:

  • Save 失败时,仍会执行 GameFlow shutdown 和 preload release。
  • GameFlow shutdown 失败时,仍会执行 preload release。
  • Shutdown 失败后再次调用不会直接跳过必要清理。

P1. UIPreloadService 并发去重会把首个调用者取消传播给所有等待者

风险等级:高
建议:进入 P2 前修正。

证据:

文件:My project/Assets/FlowScope/Runtime/UI/UIPreloadService.cs

当前流程:

if (_inFlightLoads.TryGetValue(panelType, out var inFlightLoad))
{
    await inFlightLoad;
    return;
}

var loadTask = LoadAndStoreAsync(panelType, path, cancellationToken);
_inFlightLoads.Add(panelType, loadTask);
await loadTask;

问题:

  • 首个调用者的 cancellationToken 被用于底层 LoadAndStoreAsync
  • 后续调用者只 await 同一个 inFlightLoad
  • 如果第一个调用者取消,后续未取消的调用者也会失败。
  • 这与 ResourceService 已经建立的共享加载语义不一致:调用者取消应只取消自己的等待,不应取消共享底层加载。

建议修法:

  1. _inFlightLoads 不应直接绑定首个调用者的 token。
  2. 底层 preload load 可使用独立生命周期 token 或不可取消共享任务。
  3. 每个调用者自己的 token 只控制自己的等待。
  4. 如果所有等待者都取消,再决定是否取消或释放底层任务,语义要和 ResourceService 对齐。

建议补测:

  • 第一个 PreloadAsync<TPanel>(cts.Token) 取消,第二个 PreloadAsync<TPanel>(CancellationToken.None) 仍成功。
  • 第二个等待者取消,不影响第一个等待者成功。
  • 所有等待者取消后,最终 handle 不被缓存或被正确释放。

仍未完全闭环但可后置的风险

Addressables 真实 load/release 验证仍然较薄

当前 AddressablesResourceBackendTests 仍主要是 Name_ReturnsAddressables
这不是本轮整改新引入的问题,但它仍然是 P2 包分发前的验收缺口。

建议:

  • 如果短期不方便制作 Addressables 测试资源,至少补一条人工验收记录。
  • 若准备进入 P2 包生态,建议补最小 Addressables PlayMode 集成测试:
    • load 成功。
    • release 被调用。
    • load 失败路径可诊断。
    • caller cancellation 语义与 ResourceService 不冲突。

文档状态

本轮已更新:

  • docs/guides/flowscope-runtime-usage.md
  • docs/requirements/p1-production-hardening.md
  • docs/reviews/flowscope-framework-review.md
  • docs/reviews/p1-completion-report.md
  • docs/reviews/p1-architecture-audit-before-p2.md

评价:

  • P1 completion report 中旧的 Addressables 编译阻塞状态已经同步。
  • runtime usage guide 已明确 RegisterFactory 是兼容 lazy singleton不推荐用于 Feature。
  • 但上一份 p1-architecture-audit-before-p2.md 仍保留原始问题主体,同时在顶部记录“第一轮 P2 前最小整改”。这可以接受,因为它是历史审核文档;后续执行应以本文的复审问题为准。

最新轻量验证

本次复审后执行:

dotnet build "My project\FlowScope.Tests.EditMode.csproj" --no-restore
dotnet build "My project\FlowScope.Tests.PlayMode.csproj" --no-restore

结果:

  • EditMode generated csproj build0 error。
  • PlayMode generated csproj build0 error。
  • 仍有 Unity 生成 csproj 的引用版本冲突 warning。

说明:

  • 以上验证只能证明生成的 C# 项目可编译。
  • 本文未重新执行 Unity Test Runner。
  • 本文未做 MainMenuP0 人工场景验收。

P2 Gate 判断

当前不建议立刻进入 P2。

原因:

  1. ShutdownAsync 的失败清理问题会影响样例作为官方 Bootstrap 模板的可信度。
  2. UIPreloadService 并发取消语义与 ResourceService 不一致,属于框架使用者容易踩到的生命周期问题。

建议最小进入 P2 条件:

  1. 修复 GameBootstrap.ShutdownAsync best-effort 清理。
  2. 修复 UIPreloadService per-caller cancellation 与共享 preload 语义。
  3. 补对应 EditMode / PlayMode 测试。
  4. 保持当前 generated csproj build 0 error。
  5. 如果 P2 第一项是 package/editor tooling至少补一次 Unity Test Runner 或明确记录无法执行原因。

后续执行建议

优先级顺序:

  1. 先修 ShutdownAsync 的失败清理和重入状态。
  2. 再修 UIPreloadService 的 in-flight cancellation 语义。
  3. 补测试。
  4. 再讨论是否进入 P2。

不要在这轮顺手扩展:

  • Luban。
  • YooAsset / AssetBundle。
  • 云存档。
  • AudioMixer / 3D 音频。
  • 完整 UI 路由。
  • Package 分发和编辑器工具。