一、Compose 初始组合阶段原理

初始组合(Initial Composition)是 Compose 声明式 UI 的起点。当 Composable 函数首次被调用时,系统会执行完整的 Composition 流程,建立 SlotTable 并生成初始的 Composition 树。理解此阶段原理,是优化冷启动与首帧耗时的基础。

1.1 Composition 阶段执行流程

初始组合分为三个核心步骤:生成 Composable 代码(编译期)、执行 Composition(运行时)、构建 SlotTable(存储层)。

关键概念:Composition 阶段不涉及实际绘制,只负责描述 UI 结构。它接收 Composable 函数,输出不可变的 SlotTable,供后续 Layout 与 Drawing 阶段消费。

详细流程如下:

  1. Compiler Plugin 生成:Kotlin Compiler Plugin 将 Composable 函数体转换为可暂停的字节码,并插入 SlotTable 读写指令。
  2. Composer 对象创建:每个 Composable 调用对应一个 Composer 实例,它持有指向当前 SlotTable 位置的指针。
  3. 槽位写入:Composer 将 UI 节点(如 Text、Button)的结构信息写入 SlotTable 的槽位(Slot)中。
  4. 分组与锚点:使用 Group 标记嵌套关系,用 Anchor 建立可追踪的引用点,支撑后续重组时的增量更新。
  5. SlotTable 闭环:Composition 完成后,SlotTable 固化,等待 Recomposer 调度 Layout 阶段。
性能要点:初始组合阶段的所有节点写入都是全量操作。每个 Composable 函数体中的 if/else、for 循环都会产生对应的 Group 节点。减少不必要的嵌套 Composable 调用可直接降低初始组合耗时。

1.2 SlotTable 结构

SlotTable 是 Compose 运行时的核心数据结构,它将 UI 树信息存储在扁平化的数组中,而不是传统的嵌套对象树。结构包含以下层级:

层级 角色 存储内容
Slots(基槽) 存储单个数据值 Composable 参数、状态快照引用、key 值
Groups(分组) 定义节点范围与嵌套 开始偏移、结束偏移、父节点索引
Nodes(节点) 实际 UI 对象引用 LayoutNode、AndroidView 等对象指针
Anchors(锚点) 可移动的追踪标记 指向特定 Slot 位置的索引,重组时用于定位

初始组合时,所有分组与槽位第一次被分配并填充。组信息、槽位值、节点引用三者通过索引数组紧密关联,避免对象头开销。

常见误区:SlotTable 不是 Diff 对象,它不存储前后对比数据。重组时,Recomposer 会重新读取该 Composable 对应的 Slot 区域,用新值覆盖旧槽位,而非创建副本。这意味着初始组合写入的槽位会在整个组合树生命周期内被复用。

1.3 Recomposer 调度机制

Recomposer 负责驱动 Composition 阶段的执行与重新执行。其内部维护了一个调度队列,处理来自状态变化的 recomposition 请求。对于初始组合,Recomposer 处理首次构建任务:

  • 根 Composable 入队:setContent {} 中的根 Composable 被包装为 Recomposition 任务。
  • 帧边界同步:初始组合在下一帧的 composition 阶段前完成,确保首帧可见。
  • 优先级处理:初始组合任务具有最高隐式优先级,不可被重组任务抢占。
  • 线程模型:初始组合在 Main 线程执行,遵循单调帧时钟。

查看 Recomposer 调度日志:

// 开启 Compose 调度跟踪
adb shell setprop debug.compose.trace true
// 过滤 Recomposer 日志
adb logcat -s "Compose" "Recomposer"
// 典型初始组合日志示例
D Compose: [Composition] Starting initial composition for 'RootComposable'
D Compose: [Composition] Group push: Column@12345678
D Compose: [Composition] Slot write: null (initial)
D Compose: [Composition] Slot write: Modifier@abcdef12
D Compose: [Composition] Group pop
D Compose: [Composition] Composing took: 1.2ms

1.4 初始组合与重组对比

维度 初始组合 重组
触发条件 Composable 首次被调用 状态(State/MutableState)变化
代码执行 完整执行 Composable 函数体 仅执行受状态影响的分支
SlotTable 操作 全量分配槽位并写入初始值 使用 SlotTable 的定位与覆盖机制
调度方式 同步执行,不可取消 异步调度,可被新请求取消/合并
性能特征 线性时间 O(N),N 为节点总数 子线性时间,跳过未变节点
调试标记 ComposeInitialComposition ComposeRecomposition

1.5 初始组合阶段知识结构图

下图展示了从 Composable 代码到 SlotTable 的数据流,以及 Recomposer 在其中的调度角色:

初始组合阶段流程与数据结构 Composable 函数 @Composable fun Screen() Compiler Plugin 插入 Slot 读写指令 Composer 实例 指向当前 Slot 指针 SlotTable 写入 分组 / 槽位 / 节点 锚点注册 Recomposer 调度 帧边界同步 初始组合完成 SlotTable 固化 🔍 初始组合性能关注点 • 每个 Composable 节点产生一个 Group 对象,紧凑嵌套可减少 SlotTable 扫描深度 • 初始组合阶段所有 Lambda 依赖都会被注册,状态粒度影响后续重组范围 • 使用 Layout Inspector 的 Compose Inspector 可以查看组合完成的 SlotTable 大小 执行组合 编译期 运行时

1.6 调试与验证方法

获取初始组合耗时数据,最直接的方式是使用 Compose Compiler Metrics:

启用 Compiler Metrics 命令:
// 在 build.gradle.kts 中配置
composeOptions {
  kotlinCompilerExtensionVersion = "1.5.12"
}
// 在 gradle.properties 中开启
kotlin.compiler.compose.metricsDestination=build/compose-metrics
kotlin.compiler.compose.reportsDestination=build/compose-reports
// 阅读初始组合相关指标
// 文件位置:build/compose-metrics/composable-function-metrics.txt
// 关键字段:
// Function: com.example.app.MainScreen
// GroupCount: 124
// SlotCount: 356
// InitialCompositionDuration: 3.45ms

运行时跟踪使用 Compose Tracing:

// 全局启用 Compose Tracing
android:debuggable="true"
// 查看 Systrace 中 Composition 阶段
python systrace.py --time=10 -a com.example.app compose

1.7 常见错误与避坑

  • 过度组合(Over-Composition):在初始组合期间执行重计算操作(如排序、过滤),导致 SlotTable 写入被阻塞。建议将计算延迟到重组或 side-effect 中。
  • 隐式递归:Composable 函数中调用自身而缺少 base case,导致 StackOverflow 与初始组合无法完成。验证方法是使用 `-Xss` 参数检查栈深度。
  • slotTable 空指针:在未完成初始组合的 Composer 上调用 `setContent` 再次触发初始组合。保持单次 setContent 调用,使用 CompositionLocal 传递根依赖。
  • Inline Composable 误用:`@Composable inline` 函数会导致 SlotTable 缺少独立的 Group,阻碍重组时的跳过机制。默认不使用 `inline` 修饰 Composable 函数。

1.8 稳定性与优化第一步

初始组合耗时主要受 Composable 节点数量和SlotTable 深度影响。优化起点:

  • 使用 Layout Inspector 查看组合树,计算节点数。
  • 对 Column/Row 中超过 10 个的同类型子元素,考虑 LazyColumn 替代。
  • 检查 modifier 参数是否包含不必要的对象分配(如每次重组创建 lambda)。
  • 在初始组合中避免调用 LaunchedEffect/SideEffect 以外的副作用,这些副作用会延长 SlotTable 写入阶段。