424 lines
16 KiB
Markdown
424 lines
16 KiB
Markdown
# 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 限制文档或验证。
|
||
|
||
---
|
||
|
||
## 最新验证证据
|
||
|
||
本次审核阶段只做代码阅读和轻量编译验证,没有修改运行时代码。
|
||
|
||
已执行:
|
||
|
||
```powershell
|
||
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.cs`
|
||
- `ForFactory` 创建的 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.cs`
|
||
- `CreateFeature<TFeature>` 创建 feature scope 后调用 `scope.Resolve<TFeature>()`。
|
||
- `My project/Assets/FlowScope/Samples/MainMenuP0/Scripts/GameBootstrap.cs`
|
||
- `MainMenuFeature` 注册在 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。
|
||
|
||
整改建议:
|
||
|
||
1. 明确 Container 生命周期模型,至少区分:
|
||
- singleton/root service
|
||
- scoped/feature-owned service
|
||
- transient/new instance per resolve
|
||
2. 调整 API 命名,避免 `RegisterFactory` 继续误导:
|
||
- 方案 A:保留现有缓存语义,但改名或补充 `RegisterSingletonFactory`,另加 `RegisterTransient`。
|
||
- 方案 B:让 `RegisterFactory` 真正每次 resolve 创建新实例,再另加 singleton API。
|
||
3. 明确父注册在子 scope 下解析时的创建者与释放者:
|
||
- 若是 singleton,应由注册所在 scope 持有。
|
||
- 若是 scoped/transient,应由请求 scope 持有。
|
||
4. 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、预加载、场景切换,会更难判断谁拥有启动任务列表。
|
||
|
||
整改建议:
|
||
|
||
二选一:
|
||
|
||
1. GameFlow 退回纯 Feature 编排器:
|
||
- 移除或策略化 `IConfigProvider.LoadAllAsync`。
|
||
- Config/Save/Data 由 Bootstrap 或 AppStartupPipeline 负责。
|
||
2. GameFlow 正式成为 AppFlow:
|
||
- 引入明确启动任务/保存任务策略。
|
||
- 将 Data load/save 纳入可配置流程,而不是只硬编码 Config。
|
||
|
||
最低验收:
|
||
|
||
- 文档和代码一致说明:Config 加载、Save 加载、Save 写回分别由谁负责。
|
||
- 样例不再与主文档互相矛盾。
|
||
- GameFlow 测试覆盖职责定案后的启动/关闭行为。
|
||
|
||
---
|
||
|
||
### 4. 样例 Bootstrap 使用 `async void`,容易被业务照抄
|
||
|
||
风险等级:高
|
||
建议:P2 前修。
|
||
|
||
证据:
|
||
|
||
- `GameBootstrap.Awake()` 是 `async void` 并直接 await `StartupAsync`。
|
||
- `GameBootstrap.OnApplicationQuit()` 是 `async void` 并直接 await `ShutdownAsync`。
|
||
|
||
影响:
|
||
|
||
- 启动异常可能成为未观察异常或只表现为 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` 只保存 `Action` close 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.md`
|
||
- `docs/requirements/p1-production-hardening.md`
|
||
- `docs/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.md`
|
||
- `docs/reviews/p1-completion-report.md`
|
||
|
||
不一致点:
|
||
|
||
- 旧评审提到 AudioService 同步阻塞异步加载。
|
||
- 当前实现已经提供 `PlayBgmAsync` / `PlaySfxAsync`,同步方法只允许读取 `ICompletedResourceService` 已完成资源,不再 `.GetAwaiter().GetResult()` 阻塞。
|
||
|
||
建议:
|
||
|
||
- 更新文档:旧同步阻塞风险已降低。
|
||
- 新风险应改为:同步 API 只适合预加载完成资源,使用者必须理解并处理异常。
|
||
|
||
### C. GameFlow 职责文档与实现不一致
|
||
|
||
文档位置:
|
||
|
||
- `docs/requirements/p0-requirements-set.md`
|
||
- `docs/requirements/p0-gameflow.md`
|
||
- `docs/guides/flowscope-runtime-usage.md`
|
||
|
||
不一致点:
|
||
|
||
- 文档把 GameFlow 写成全局生命周期所有者。
|
||
- 实现中保存、Data 加载、预加载释放主要由 Bootstrap 负责。
|
||
- GameFlow 只硬编码 Config 加载和 Feature 生命周期。
|
||
|
||
建议:
|
||
|
||
- 先做架构决策,再同步文档。
|
||
|
||
---
|
||
|
||
## P2 前最小整改清单
|
||
|
||
必须先修:
|
||
|
||
1. 修正或定案 Container 生命周期语义。
|
||
2. 保证 Feature 每次进入都是新实例,且 Feature scope 拥有自己的短生命周期对象。
|
||
3. 明确 GameFlow 与 Bootstrap 职责边界,并让代码和文档一致。
|
||
4. 修正样例 Bootstrap 的 `async void` 启动/退出风险。
|
||
5. 更新 P1 completion report / runtime usage guide / framework review 中的过时状态。
|
||
|
||
建议同步补测:
|
||
|
||
1. Container:singleton / scoped / transient 行为。
|
||
2. GameFlow:`A -> B -> A` 重入。
|
||
3. ResourceService:共享加载取消边界。
|
||
4. UIPreloadService:并发预加载与释放语义。
|
||
5. Addressables:真实 load/release 最小 PlayMode 验证。
|
||
|
||
完成以上后再评估 P2。
|
||
|
||
---
|
||
|
||
## 执行 session 建议顺序
|
||
|
||
1. 先只改 Container 和 GameFlow 相关语义,不碰 P2 包分发。
|
||
2. 跑 EditMode 测试,确认核心生命周期稳定。
|
||
3. 再改样例 Bootstrap,让 MainMenuP0 继续作为真实使用样板。
|
||
4. 跑 PlayMode 测试,确认 UI、样例、资源释放没有退化。
|
||
5. 最后更新文档,确保 report 不再保留旧阻塞状态。
|
||
|
||
---
|
||
|
||
## 禁止事项
|
||
|
||
- 不要在本轮顺手进入 P2。
|
||
- 不要扩大到 Luban、YooAsset、云存档、AudioMixer、完整 UI 路由。
|
||
- 不要为了修测试而弱化 Feature scope 或资源释放语义。
|
||
- 不要只改文档掩盖 Container 生命周期问题。
|
||
- 不要把样例变成特殊路径;样例应继续代表推荐使用方式。
|