跳到主要内容

ADR-0015:可观察属性事务

背景

ViewCompose 已支持不依赖编译器的分组重组。显式边界可以跳过稳定的声明体,新 VNode 到达 Android Renderer 后也能原地 patch 复用的 View。但是,当一次 State 读取决定许多叶子属性时, 它仍会让祖先声明失效、重建 VNode 结果,并把完整 Root 树送入 reconcile。

已验收的复杂布局对照暴露了后果。ViewCompose 的中位数低于直接 Android Views control, 但 P95 为 41.187 ms,原生只有 16.222 ms。Perfetto 没有发现锁、I/O 或前台 GC 停顿;慢帧 同步执行完整声明、树 diff 和 Android View traversal,落到 LITTLE CPU 后成本进一步放大。 局部分配、物理树、子树证明、reconcile 和编译状态实验都没有实质缩小尾部差距。

项目无法使用 Compose 编译器生成 changed flags 和 restart lambda,因此不能推断哪些 Kotlin 表达式可以独立安全地重跑,也不能对任意 capture 承诺自动属性跳过。但框架可以提供显式契约, 并让其所有权和失败语义与现有 Snapshot、RenderSession、VNode 和原生回滚模型一致。

决策

ViewCompose 增加渲染器中立的可观察属性事务,作为显式 Q3 能力。

可观察值或可观察 NodeSpec 声明同步 reader 与显式普通 inputs。reader 中的 Snapshot State 读取属于 Session 所有的属性观察,而不是外围 Composer scope。inputs 相等时可以继续使用已提交 reader 和值;非 State capture 变化却未更新 inputs 属于不支持用法,这是没有编译器时必须公开 的边界。

每个 RenderSession 的 Registry 独占全部观察。它把属性源关联到发出它的逻辑 scope,捕获当前 Local 环境、合并失效,并在一个固定 Snapshot 中读取全部 dirty source;依赖和值的变化在发布前 保持候选状态。完整组合以事务方式 reconcile Registry 成员。只有逻辑所有权结束后,移除和 Session dispose 才释放观察。

可观察属性只能替换同一逻辑节点的 NodeSpec。节点 type、key、Modifier、children 与环境所有权 都是结构信息,必须走完整组合。违反契约时应报告错误,不能静默回退,否则既会隐藏过时 capture, 也会隐藏无上界的性能。

成功的完整 Renderer frame 返回不透明属性 target。属性 frame 直接定位这些 target,比较前后 VNode,绝不 reconcile sibling 或 child。Renderer 对整个批次做 preflight 和 checkpoint。任何 reader 或原生绑定失败都会放弃候选观察,并在旧属性值继续作为权威值之前恢复此前的全部原生 变化。只有整个批次成功后才运行 commit effect。

框架环境变化仍由 Host 所有。locale、资源、主题、density、font scale 或 layout direction 变化 触发的完整 render 会替换捕获的 Local snapshot 与 reader。结构和属性工作合并时,结构 frame 优先,并在现有单一 frame 边界提交两类候选。

首个类型化组件接入是可观察 Text 内容,同时提供渲染器中立的低层可观察 NodeSpec 路径供自定义 节点使用。后续类型化接入必须复用同一 Registry 和 Renderer 事务,不得增加组件专属 Listener 或 Renderer 所有的 State 订阅。

结果

  • 属性密集型 State 更新可以接近保留 Android View 的直接修改,同时保留声明式来源和原生回滚契约。
  • 显式 inputs 取代编译器稳定性推断;API 比 Compose 语法更刻意,但行为可预测。
  • 结构更新仍然更昂贵并单独测量;可观察属性不会形成第二套隐藏子树模型。
  • VNode 与 Core Render SPI 增加不透明属性身份和精确 target 事务概念。
  • RenderSession 是唯一可以组合 composition、property、native、effect、overlay 与 diagnostic 提交顺序的协调器。
  • Renderer 必须原子实现可观察属性 patch,或者拒绝启用该能力,不能部分应用批次。
  • 调试工具可以描述属性 patch,但根据 ADR-0009,未激活工具仍不能进入热路径。

未采用的方案

在 Android Binder 中增加 State 重载和 Listener

不采用,因为 Android View 会拥有逻辑观察,dispose 和依赖变化会逃逸 RenderSession,其他 Renderer 也会分叉,原生回调还可能在回滚和 Effect 顺序之外修改已提交 frame。

把每个 RecomposeBoundary 当成属性事务

不采用,因为边界可以发出零个、一个或多个节点,也可以改变结构。独立更新它们需要第二套子树 所有权和锚点模型。它们继续作为显式结构重启原语。

继续重跑 Root,只优化更多比较

不采用,因为测量已经否决。多轮比较、分配、分组、物理深度和编译实验都没有实质改善 P95; 在直接 Android 修改结束前,算法仍然访问完整更新表面。

从任意 State capture 自动推断属性

不采用,因为项目没有 Compose 编译器、changed flags、稳定性推断或安全的 restart lambda 生成。 运行时反射无法恢复同等语义。

属性改变结构时静默回退

不采用,因为这会让有界 API 变为依赖 workload 的整树工作,并可能隐藏错误的 inputs 列表。 契约错误必须可观察,并给出明确的结构式替代路径。

公开 API 与模块影响

  • viewcompose-ui-contract 在 VNode 上携带不透明可观察属性身份。
  • viewcompose-ui-foundation 拥有 Q3 可观察值、可观察 NodeSpec 发出、候选 Registry、frame 调度、失败报告和 Core Render SPI。
  • viewcompose-host-android 转换 Core property frame,且不向上暴露 Android View。
  • viewcompose-renderer-android 拥有精确 Mounted target 索引、Binder preflight、原子 apply、 rollback、commit effect 与诊断。
  • Demo 与 benchmark fixture 分离属性和结构更新 action,并在一个带 revision 的 workload 契约下比较 ViewCompose、Compose 与 Android Views。

验证与发布

实现证据保留在 可观察属性事务归档计划。 硬切要求 Q3 KDoc 与编译样例、Fake Engine 和 Android 失败注入、Local 与资源变化测试、生命周期与 dispose 测试、API 和模块文档、中文镜像、单一 release changeset、已验收的三引擎 benchmark 证据,以及仓库全部质量门禁。