ViewCompose 状态快照
1. 文档定位
本文档定义 viewcompose-runtime 状态系统,以及 viewcompose-ui-contract 发布的 Renderer 连接型
状态 Owner 的 Snapshot 语义与使用约束。
目标:
- 统一
MutableState的一致性读写语义 - 明确并发写入冲突处理规则
- 防止后续演进回退到“直接赋值 + 无事务”模型
2. 公开 API
mutableStateOf(value, policy)默认策略:structuralEqualityPolicy()SnapshotMutationPolicy<T>equivalent(a, b):判定是否视为无变化merge(previous, current, applied):并发冲突合并,返回null代表无法合并
SnapshottakeSnapshot()takeMutableSnapshot()withMutableSnapshot { ... }currentGlobalId()
MutableSnapshotenter { ... }apply()dispose()
RuntimeObservationobserveReads(onInvalidated) { ... }:创建一个可独立释放的依赖 Owner;prepareReplacement(previous) { ... }:读取候选依赖,并返回显式commit/abort替换事务。
- Renderer 连接型状态
LazyListState:虚拟化 Item 位置与布局信息;ScrollState:Eager Container 的逻辑偏移、范围、Viewport、运动与命令;PagerState:当前页、已停稳页、目标页、偏移、页数、运动、能力与命令。
3. 核心语义
MutableState基于 MVCCStateRecord记录链实现,读取按readId选择可见版本。state.value = x在无显式 snapshot 上下文时,内部走 autocommit 事务(takeMutableSnapshot + apply)。MutableSnapshot.apply()为串行发布:- 无冲突:直接提交
- 有冲突:走
policy.merge(previous, current, applied) - merge 失败:
apply()返回Failure
- 读快照隔离:
Snapshot.takeSnapshot().enter { ... }始终读取该快照可见版本,不受后续全局提交影响。 ComposerLite每轮 compose 在一致性读快照中执行;同一轮内读取结果不漂移。- Runtime 跟踪活动 snapshot 的
readId;提交时保留活动读者所需版本,snapshot 释放后裁剪不再可见的历史记录。 - 一次成功的全局 Apply 会按稳定顺序去重受影响的
Observation,并在释放 Runtime 与 State Lock 后,最多在 Apply 线程调用每个 Observation 一次。不同 Apply 不会合并;冲突或无操作 Apply 不发送失效通知。 - 构成一个公开逻辑元组的框架字段必须使用一次现有 Mutable Snapshot Transaction。
synchronized等 Writer 串行化手段不能让多个独立 Commit 对 Snapshot Reader 原子可见。 - Renderer 连接型 State 通过普通
MutableState发布一个不可变 Snapshot。相等 Snapshot 不会 使 Observation 或 Listener 失效。 - 同一时间只允许一个活动 Connector。替换时先捕获旧 Connector 的最新 Snapshot、清除其 Listener,再连接新 Connector。释放时断开连接;陈旧命令不得到达已放弃的原生 View。
ScrollState.scrollTo会保留 Detach 目标,并在新 Eager Host Attach 后应用;animateScrollTo在 Detach 时无操作。PagerState命令在 Detach 时无操作,因为受控 Pager 声明在重建后仍是权威来源。- Eager 横向偏移与 Pager 索引在 RTL 中使用逻辑顺序。原生物理位置属于 Renderer 细节,不能 泄漏到可移植 Snapshot。
- Observation 依赖替换会保留与已提交依赖集合共有的订阅。候选独有依赖会临时订阅同一个
Observation;即使一次 Apply 同时修改旧依赖与候选依赖,也能保留最多一次的 Callback Identity。commit在没有失效空窗的情况下切换权威集合,abort则只释放候选新增项。同一 时间只能 Prepare 一个替换,且每个替换都必须执行一个终止操作。 - 嵌套快照固定创建时父快照可见的待提交值,后续父写入不会改变子快照的读取视图。子快照提交时 将各状态的写入身份与该基线比较:较早的父写入不构成冲突,之后的写入才构成冲突。不能向已提交 或已释放的父快照提交;只读子快照也保留父方的待提交值。
- 派生缓存区分可变快照身份和本地写入;读取相同已提交历史的只读快照可以共享缓存有效性。 只有存在消费者时才保留上游订阅;独立读取会验证视图且不保留上游订阅,新消费者在读取返回前 重新连接依赖。
- 成功发布在通知前使可变快照进入终态。一次提交会尝试通知所有受影响的观察者,包括派生依赖 路径,且每个观察者最多通知一次。回调失败时重新抛出首个异常,并将后续异常加入 suppressed; 写入已经提交,不能按未提交事务重试。重入提交使用独立通知批次。
- 派生计算失败会保留之前的依赖订阅。即使缓存已失效,之后的提交仍会产生失效通知机会。
4. 并发与冲突约束
- 冲突判定以状态记录版本为准:目标状态在事务
readId后产生新记录时视为并发写入。 equivalent(a, b)只负责判断一次赋值是否产生新记录,不用于推断事务是否并发。- 冲突默认不覆盖;仅当
merge提供可合并值时才可提交。 - 未提供 merge 能力(默认 policy)时,冲突应失败,由上层重试策略决定下一步。
- 嵌套冲突比较待提交写入的身份,而不是值相等性。父方写回原值仍属于后续写入。子快照只捕获 待提交值,已提交记录通过固定的 read ID 共享;捕获具有待提交写入的父视图时,空间成本与该 待提交集合成正比,而不是与所有存活状态对象成正比。
5. 开发约束
- 禁止在 runtime 新增绕过 snapshot 的状态写入路径。
- 新状态容器若接入
RuntimeObservation,必须实现 snapshot 可见性语义。 - 修改策略或冲突语义时,必须同步补齐并发事务单测与 compose 一致性单测。
Snapshot/MutableSnapshot使用完成后必须调用dispose()或通过use关闭,避免长期保留历史版本。- 新增多个框架可观察字段时,必须先判断它们是一个 Invariant 还是独立事件。只把同一 Invariant
的写入放进
Snapshot.withMutableSnapshot,并用失效 Callback 读取完整元组进行测试。 - State Connector 变更必须覆盖替换、释放、相等 Snapshot、Pending Command 与逻辑 RTL 测试。 State 对象不得持有 Android View 或执行平台配置。
- 框架事务重新计算长期存在的 Observed Reader 时,必须使用 Prepared Dependency Replacement。 每个成功帧都释放并重建全部订阅,既会引入竞态风险,也会在热路径产生重复工作。
6. 关联文档
全局提交通知会暂时离开调用方的快照上下文,读取已提交的全局状态,并可发起独立事务;
finally 中恢复调用方原有上下文。即使在 enter() 内调用 apply(),通知仍能读取提交结果,
而返回后在终态快照内继续读取仍然无效。