一、Compose 初始组合阶段原理
初始组合(Initial Composition)是 Compose 声明式 UI 的起点。当 Composable 函数首次被调用时,系统会执行完整的 Composition 流程,建立 SlotTable 并生成初始的 Composition 树。理解此阶段原理,是优化冷启动与首帧耗时的基础。
1.1 Composition 阶段执行流程
初始组合分为三个核心步骤:生成 Composable 代码(编译期)、执行 Composition(运行时)、构建 SlotTable(存储层)。
关键概念:Composition 阶段不涉及实际绘制,只负责描述 UI 结构。它接收 Composable 函数,输出不可变的 SlotTable,供后续 Layout 与 Drawing 阶段消费。
详细流程如下:
- Compiler Plugin 生成:Kotlin Compiler Plugin 将 Composable 函数体转换为可暂停的字节码,并插入 SlotTable 读写指令。
- Composer 对象创建:每个 Composable 调用对应一个 Composer 实例,它持有指向当前 SlotTable 位置的指针。
- 槽位写入:Composer 将 UI 节点(如 Text、Button)的结构信息写入 SlotTable 的槽位(Slot)中。
- 分组与锚点:使用 Group 标记嵌套关系,用 Anchor 建立可追踪的引用点,支撑后续重组时的增量更新。
- 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 在其中的调度角色:
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 写入阶段。