16 KiB
FlowScope P1 后架构审核与 P2 前置整改清单
日期:2026-05-21
范围:FlowScope Game Core 当前 P0/P1 实现、P0/P1 需求文档、P1 completion report、runtime usage guide。
目的:作为后续执行 session 的整改依据。本文只定义问题、风险、建议和验收标准,不直接展开 P2 实施。
结论
当前 FlowScope 已经形成 P0/P1 轻量 Unity 游戏框架骨架,但不建议直接进入 P2。
核心原因不是功能数量不足,而是若干 P0/P1 内核语义还没有稳定:Container 生命周期、Feature scope、GameFlow 与 Bootstrap 职责、样例启动退出模型、资源/UI 取消与预加载边界、文档状态一致性。P2 的包分发、编辑器工具和跨项目复用会固化这些 API,若此时推进,后续修正成本会显著升高。
建议先执行“P2 前最小整改清单”,确认内核语义稳定后,再进入 P2。
整改进展(2026-05-21)
已执行第一轮 P2 前最小整改:
- Container 已明确
RegisterSingletonFactory/RegisterScoped/RegisterTransient三类生命周期,RegisterFactory仅保留为旧式 lazy singleton 兼容入口。 - Feature 示例和测试改为 transient 注册,
GameFlow的A -> B -> A重入会创建新的 Feature 实例,Feature 释放归属 Feature scope。 - MainMenuP0
GameBootstrap已移除async void Awake/async void OnApplicationQuit的直接 await 模式,启动任务通过StartupTask可观察,退出异常集中记录。 - ResourceService 已补共享加载取消边界测试,覆盖单个等待者取消、全部等待者取消后完成即释放且不缓存。
- UIPreloadService 已补同 Panel 并发预加载合并测试,并实现 in-flight load 复用。
- P1 completion report、runtime usage guide、framework review、P1 需求验收状态已同步当前代码状态。
仍未完成:
- Unity Test Runner 全量 EditMode / PlayMode 验收记录。
- Addressables 真实 load/release 最小集成测试或人工验收步骤。
UIManager运行时DestroyImmediate风险收口。- Config/Save DTO 与 AOT/linker 限制文档或验证。
最新验证证据
本次审核阶段只做代码阅读和轻量编译验证,没有修改运行时代码。
已执行:
dotnet build "My project\FlowScope.Runtime.csproj" --no-restore
dotnet build "My project\FlowScope.Tests.EditMode.csproj" --no-restore
dotnet build "My project\FlowScope.Tests.PlayMode.csproj" --no-restore
结果:
- 三个
dotnet build均为 0 error。 - 存在 Unity 生成项目的引用版本冲突 warning,主要来自
System.Net.Http、System.Security.Cryptography.Algorithms、System.ComponentModel.Annotations。 - 未在本次审核中重新执行 Unity Test Runner,因此不能用本文替代 Unity EditMode / PlayMode 测试结果。
阻塞级 / 高风险问题
1. Container 工厂生命周期与 Feature scope 语义冲突
风险等级:阻塞级
建议:P2 前必须修。
证据:
My project/Assets/FlowScope/Runtime/Container/ContainerRegistration.csForFactory创建的 registration 会缓存_instance。Resolve()中_created == true后直接返回旧实例。- 工厂创建的
IDisposable被_owner.TrackOwnedDisposable跟踪。
My project/Assets/FlowScope/Runtime/Container/Container.cs- 子 scope 找不到注册时会递归返回父 scope 的 registration。
My project/Assets/FlowScope/Runtime/Flow/GameFlow.csCreateFeature<TFeature>创建 feature scope 后调用scope.Resolve<TFeature>()。
My project/Assets/FlowScope/Samples/MainMenuP0/Scripts/GameBootstrap.csMainMenuFeature注册在 root:_root.RegisterFactory(_ => new MainMenuFeature(...))。
真实行为:
RegisterFactory名义上像“工厂”,实际是“懒加载单例”。- root 注册的 Feature 被 feature scope 解析时,仍使用 root registration。
- 同一个 Feature 类型如果再次进入,可能复用已经
Dispose过的旧实例。 - 由父 scope registration 创建的对象由父 scope 持有释放,削弱 Feature scope 的生命周期隔离。
影响:
- 破坏 P0 文档中的“GameFlow 创建 Feature scope,Feature 临时对象随 FeatureContext 释放”。
- Feature 之间可能共享不该共享的实例状态。
- P2 包化后,使用者会照着
RegisterFactory注册 Feature/ViewModel/临时对象,埋下复用已释放对象的隐性 bug。
整改建议:
- 明确 Container 生命周期模型,至少区分:
- singleton/root service
- scoped/feature-owned service
- transient/new instance per resolve
- 调整 API 命名,避免
RegisterFactory继续误导:- 方案 A:保留现有缓存语义,但改名或补充
RegisterSingletonFactory,另加RegisterTransient。 - 方案 B:让
RegisterFactory真正每次 resolve 创建新实例,再另加 singleton API。
- 方案 A:保留现有缓存语义,但改名或补充
- 明确父注册在子 scope 下解析时的创建者与释放者:
- 若是 singleton,应由注册所在 scope 持有。
- 若是 scoped/transient,应由请求 scope 持有。
- GameFlow 创建 Feature 时应确保每次进入得到新 Feature 实例,不能复用已 Dispose 实例。
最低验收:
- 新增测试:
A -> B -> A,第二次进入A必须是新实例。 - 新增测试:root 注册工厂,child scope resolve 时生命周期归属符合文档。
- 新增测试:Feature scope dispose 后,不影响 root singleton,但会释放 scoped/transient 对象。
- 新增测试:已 Dispose 的 Feature 不会在再次切换时被复用。
- 更新
docs/guides/flowscope-runtime-usage.md的 Container 注册规则。
2. RegisterFactory API 名称和实现语义不一致
风险等级:高
建议:P2 前必须修或正式写入限制。
证据:
- P0 文档把
RegisterFactory<T>(Func<Container, T> factory)作为显式工厂注册。 - 实现中该 factory 只会在第一次 Resolve 时执行,后续返回缓存实例。
影响:
- 使用者自然会认为 factory 是“每次创建”或至少“不缓存业务对象”。
- 目前它更接近 lazy singleton。
- 若 P2 后作为公开包 API 发布,后续纠正会是破坏性变更。
整改建议:
- 不要只补文档,应优先调整 API 语义或新增明确 API。
- 如果为了 P1 兼容暂不破坏旧接口,也要让旧接口行为在文档中被明确标注,并禁止用于 Feature/ViewModel/短生命周期对象。
最低验收:
- Container 测试中明确覆盖 singleton/transient/scoped 三类行为。
- 文档中出现清晰示例:root service 如何注册,Feature 如何注册,临时对象如何注册。
3. GameFlow 与 Bootstrap 的生命周期职责边界不一致
风险等级:高
建议:P2 前必须定案。
证据:
- P0 文档描述 GameFlow 职责包括启动时加载或创建 Data、关闭时保存必要数据。
- 当前
GameFlow.StartupAsync固定调用IConfigProvider.LoadAllAsync。 - 当前保存
PlayerData在样例GameBootstrap.ShutdownAsync中执行,而不是 GameFlow。
真实架构:
- 当前 GameFlow 更像 Feature 生命周期编排器。
- Bootstrap 才是真正的应用装配、数据加载、存档保存入口。
- 但 GameFlow 又硬编码了配置加载,导致职责介于“纯编排器”和“应用启动流程所有者”之间。
影响:
- 使用者不清楚 Data/Save 应该挂在 GameFlow 还是 Bootstrap。
- P2 做模板或包分发时,启动流程样板会变得含混。
- 后续如果加入多 Feature、预加载、场景切换,会更难判断谁拥有启动任务列表。
整改建议:
二选一:
- GameFlow 退回纯 Feature 编排器:
- 移除或策略化
IConfigProvider.LoadAllAsync。 - Config/Save/Data 由 Bootstrap 或 AppStartupPipeline 负责。
- 移除或策略化
- GameFlow 正式成为 AppFlow:
- 引入明确启动任务/保存任务策略。
- 将 Data load/save 纳入可配置流程,而不是只硬编码 Config。
最低验收:
- 文档和代码一致说明:Config 加载、Save 加载、Save 写回分别由谁负责。
- 样例不再与主文档互相矛盾。
- GameFlow 测试覆盖职责定案后的启动/关闭行为。
4. 样例 Bootstrap 使用 async void,容易被业务照抄
风险等级:高
建议:P2 前修。
证据:
GameBootstrap.Awake()是async void并直接 awaitStartupAsync。GameBootstrap.OnApplicationQuit()是async void并直接 awaitShutdownAsync。
影响:
- 启动异常可能成为未观察异常或只表现为 Unity 日志。
- 退出时异步保存和释放没有统一错误处理入口。
- 这是官方样例,使用者很可能照抄。
整改建议:
- Bootstrap 应有可观察的启动任务和错误处理策略。
Awake可只创建 lifetime token,Start或显式RunAsync负责启动。- 异常应写入明确日志,并让测试能够断言。
- 退出保存应考虑重复调用、取消、异常 best-effort 规则。
最低验收:
- 样例启动异常可被测试捕获或至少被统一日志记录。
- 重复 Shutdown 不重复释放、不重复保存或语义明确。
- OnDestroy / OnApplicationQuit 顺序不会造成 token 已释放后继续使用。
中风险问题
5. UI 预加载并发和释放边界偏薄
风险等级:中
证据:
UIPreloadService.PreloadAsync<TPanel>没有同类型并发去重。UIManager对预加载 handle 使用空释放句柄,实际释放依赖UIPreloadService.Release/ReleaseAll。
影响:
- 两个并发预加载可能重复加载并覆盖 handle。
- UI 实例关闭不释放预加载 handle 是合理设计,但需要更强文档和测试防误用。
整改建议:
- 为
UIPreloadService增加同 Panel 类型的 in-flight load 合并。 - 增加测试:并发 Preload 只触发一次底层加载。
- 增加测试:已打开预加载 Panel 时提前 Release 的行为被定义清楚。
6. UIScreenNavigator 是轻量栈,不应被视为完整路由
风险等级:中
证据:
UIScreenNavigator只保存Actionclose stack。- 外部
UIManager.Close<TPanel>/CloseLayer/CloseAll不会同步导航栈。
影响:
- 外部关闭后,Navigator 内部可能保留已经关闭的历史 action。
- P1 可接受,但 P2 文档不能把它包装成完整页面路由。
整改建议:
- 文档明确它只管理自己 push 的页面。
- 如果 P2 要做完整导航,应重新设计 route/state,而不是扩这个 action stack。
7. 资源共享加载取消语义还需要补边界测试
风险等级:中
证据:
ResourceService.LoadEntryAsync调用 backend 时传入CancellationToken.None。- 调用者取消只取消等待,不取消共享底层加载。
- 这是合理设计,但行为复杂。
影响:
- “所有等待者取消后,底层加载完成再释放”这类边界如果没测,容易出现泄漏、重复 release 或未观察异常。
整改建议:
- 补测试:
- 多等待者中一个取消,另一个成功拿到 handle。
- 所有等待者取消,底层随后成功,资源被释放且不留 completed cache。
- 所有等待者取消,底层随后失败,没有未观察异常或残留
_loads。
8. Addressables 适配层测试过薄
风险等级:中
证据:
AddressablesResourceBackendTests当前只测Name_ReturnsAddressables。- PlayMode 编译已能通过,但不等于真实 Addressables load/release 验证通过。
整改建议:
- 增加最小 Addressables 测试资源。
- 验证
AddressablesResourceBackend.LoadAsync<T>成功加载、取消、失败、Release。 - 如果测试资源维护成本高,至少增加一条文档化的人工验收步骤。
9. 自研 JSON / 反射 / AOT 风险仍未收口
风险等级:中
证据:
ReactivePropertyJsonConverter手写 JSON parse/write。JsonSaveSerializer使用Activator.CreateInstance。- Config mapping 使用反射
MakeGenericMethod/Invoke。
影响:
- P1 阶段可接受。
- P2 包化和移动端 IL2CPP 下,需要提前验证或明确限制。
整改建议:
- P2 前至少写清 DTO 限制:
- 必须有无参构造。
- 支持字段/属性类型范围。
- 不支持复杂 polymorphism、Dictionary、UnityEngine.Object 引用等。
- 增加 IL2CPP/linker 验证计划,或改用成熟 JSON 库并保留 R3 适配层。
文档与实现不一致
A. P1 completion report 已过时
文档位置:
docs/reviews/p1-completion-report.mddocs/requirements/p1-production-hardening.mddocs/reviews/flowscope-framework-review.md
不一致点:
- 文档仍描述 PlayMode 完整编译因 Addressables 未解析失败。
- 当前
packages-lock.json已包含com.unity.addressables。 - 本次审核运行
dotnet build "My project\FlowScope.Tests.PlayMode.csproj" --no-restore为 0 error。
建议:
- 更新完成报告,把“旧阻塞已解除”和“仍需 Unity Test Runner/人工验收确认”分开写。
- 不要继续保留“Addressables 尚未解析导致 PlayMode 编译失败”的当前状态描述。
B. AudioService 旧风险描述需要更新
文档位置:
docs/reviews/flowscope-framework-review.mddocs/reviews/p1-completion-report.md
不一致点:
- 旧评审提到 AudioService 同步阻塞异步加载。
- 当前实现已经提供
PlayBgmAsync/PlaySfxAsync,同步方法只允许读取ICompletedResourceService已完成资源,不再.GetAwaiter().GetResult()阻塞。
建议:
- 更新文档:旧同步阻塞风险已降低。
- 新风险应改为:同步 API 只适合预加载完成资源,使用者必须理解并处理异常。
C. GameFlow 职责文档与实现不一致
文档位置:
docs/requirements/p0-requirements-set.mddocs/requirements/p0-gameflow.mddocs/guides/flowscope-runtime-usage.md
不一致点:
- 文档把 GameFlow 写成全局生命周期所有者。
- 实现中保存、Data 加载、预加载释放主要由 Bootstrap 负责。
- GameFlow 只硬编码 Config 加载和 Feature 生命周期。
建议:
- 先做架构决策,再同步文档。
P2 前最小整改清单
必须先修:
- 修正或定案 Container 生命周期语义。
- 保证 Feature 每次进入都是新实例,且 Feature scope 拥有自己的短生命周期对象。
- 明确 GameFlow 与 Bootstrap 职责边界,并让代码和文档一致。
- 修正样例 Bootstrap 的
async void启动/退出风险。 - 更新 P1 completion report / runtime usage guide / framework review 中的过时状态。
建议同步补测:
- Container:singleton / scoped / transient 行为。
- GameFlow:
A -> B -> A重入。 - ResourceService:共享加载取消边界。
- UIPreloadService:并发预加载与释放语义。
- Addressables:真实 load/release 最小 PlayMode 验证。
完成以上后再评估 P2。
执行 session 建议顺序
- 先只改 Container 和 GameFlow 相关语义,不碰 P2 包分发。
- 跑 EditMode 测试,确认核心生命周期稳定。
- 再改样例 Bootstrap,让 MainMenuP0 继续作为真实使用样板。
- 跑 PlayMode 测试,确认 UI、样例、资源释放没有退化。
- 最后更新文档,确保 report 不再保留旧阻塞状态。
禁止事项
- 不要在本轮顺手进入 P2。
- 不要扩大到 Luban、YooAsset、云存档、AudioMixer、完整 UI 路由。
- 不要为了修测试而弱化 Feature scope 或资源释放语义。
- 不要只改文档掩盖 Container 生命周期问题。
- 不要把样例变成特殊路径;样例应继续代表推荐使用方式。