Compose Runtime 原理深入解析:从 Composition 到 Node Tree

📅 2024-07-03 · ⏱ 30 min · 🔬 底层原理

Compose Runtime 是 Jetpack Compose 的核心引擎,它负责将你编写的 @Composable 函数转化为实际的 UI。本文将深入探索 Runtime 的内部工作原理,包括 Composition、Composer、Applier 和 Node Tree 的协作机制。

I. Runtime 架构总览

Compose Runtime 可以分为三个核心层次:

┌─────────────────────────────────────────────────────────────┐ │ @Composable 函数 │ │ (声明式 UI 描述) │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ Composition │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ │ │ Composer │ │ Slot Table │ │ Invalidations │ │ │ │ (执行协调) │ │ (状态存储) │ │ (重组追踪) │ │ │ └─────────────┘ └─────────────┘ └─────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ Applier │ │ (将变更应用到 Node Tree) │ └─────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────┐ │ Node Tree │ │ (最终的 UI 树,如 LayoutNode 树) │ └─────────────────────────────────────────────────────────────┘

1.1 核心组件职责

II. Composition 详解

2.1 什么是 Composition

Composition 是 Compose Runtime 的核心类,它代表一次「组合」。每个 ComposeView 或 setContent 都会创建一个 Composition 实例:

// Composition 的简化结构
class CompositionImpl(
    private val parent: CompositionContext,
    private val applier: Applier<*>,
) : Composition {
    
    // Slot Table 存储组合状态
    private val slotTable = SlotTable()
    
    // 待处理的失效(需要重组的作用域)
    private val invalidations = mutableListOf<RecomposeScope>()
    
    // Composer 实例
    private val composer: ComposerImpl
    
    // 执行组合或重组
    override fun setContent(content: @Composable () -> Unit) {
        // 初始组合
        doCompose(content)
    }
    
    internal fun recompose(): Boolean {
        // 处理失效,执行重组
        return doRecompose()
    }
}

2.2 Composition 的生命周期

初始组合 (Initial Composition) │ ├── 1. 创建 Composition 实例 ├── 2. 调用 setContent(content) ├── 3. Composer 开始执行 @Composable 函数 ├── 4. 数据写入 Slot Table ├── 5. Applier 构建 Node Tree └── 6. Node Tree 渲染到屏幕 重组 (Recomposition) │ ├── 1. State 变更触发 InvalidationScope ├── 2. 调度 recompose() ├── 3. Composer 重新执行受影响的 @Composable ├── 4. 比较并更新 Slot Table ├── 5. Applier 应用差异到 Node Tree └── 6. 只更新变化的部分 销毁 (Disposal) │ ├── 1. 调用 dispose() ├── 2. 执行所有 onDispose 回调 ├── 3. 清理 Slot Table └── 4. 释放 Node Tree

III. Composer 执行机制

3.1 Composer 的角色

Composer 是执行 @Composable 函数的「指挥官」。每个 @Composable 函数编译后都会接收一个隐式的 Composer 参数:

// 原始代码
@Composable
fun Greeting(name: String) {
    Text("Hello, $name")
}

// 编译后(简化)
fun Greeting(
    name: String,
    $composer: Composer,
    $changed: Int
) {
    $composer.startRestartGroup(key = 123)
    
    if ($changed != 0 || !$composer.skipping) {
        Text("Hello, $name", $composer, 0)
    }
    
    $composer.endRestartGroup()?.updateScope {
        Greeting(name, $composer, $changed or 1)
    }
}

3.2 Composer 核心 API

interface Composer {
    // 是否正在初始组合
    val inserting: Boolean
    
    // 是否可以跳过当前作用域
    val skipping: Boolean
    
    // 开始一个 Group
    fun startRestartGroup(key: Int): Composer
    fun endRestartGroup(): ScopeUpdateScope?
    
    // 记住值
    fun <T> cache(invalid: Boolean, block: () -> T): T
    
    // 检查参数是否变更
    fun changed(value: Any?): Boolean
    
    // 发射节点
    fun <T> createNode(factory: () -> T)
    fun useNode()
    
    // 记录 Side Effect
    fun recordSideEffect(effect: () -> Unit)
}

3.3 Groups 与位置记忆

Composer 使用 Groups 来组织 Slot Table 中的数据。每个 @Composable 调用都会创建一个 Group:

@Composable
fun Counter() {
    // Group 1: Counter 函数本身
    var count by remember { mutableStateOf(0) }  // Slot: count 状态
    
    Column {  // Group 2: Column
        Text("Count: $count")  // Group 3: Text
        Button(onClick = { count++ }) {  // Group 4: Button
            Text("+")  // Group 5: 嵌套 Text
        }
    }
}
Slot Table 结构示意: ┌──────────────────────────────────────────────────────────┐ │ Group 1 (Counter) │ │ ├── Slot: remember { mutableStateOf(0) } → count = 0 │ │ │ │ │ └── Group 2 (Column) │ │ ├── Group 3 (Text) │ │ │ └── Slot: "Count: 0" │ │ │ │ │ └── Group 4 (Button) │ │ ├── Slot: onClick lambda │ │ └── Group 5 (Text) │ │ └── Slot: "+" │ └──────────────────────────────────────────────────────────┘

IV. Applier 与 Node Tree

4.1 Applier 接口

Applier 是连接 Composition 和 Node Tree 的桥梁。它定义了如何操作节点树:

interface Applier<N> {
    // 当前节点
    val current: N
    
    // 向下移动到子节点
    fun down(node: N)
    
    // 向上返回父节点
    fun up()
    
    // 插入节点
    fun insertTopDown(index: Int, instance: N)
    fun insertBottomUp(index: Int, instance: N)
    
    // 移除节点
    fun remove(index: Int, count: Int)
    
    // 移动节点
    fun move(from: Int, to: Int, count: Int)
    
    // 清空所有子节点
    fun clear()
}

4.2 UiApplier - Android 实现

在 Android Compose UI 中,UiApplier 负责操作 LayoutNode 树:

internal class UiApplier(
    root: LayoutNode
) : AbstractApplier<LayoutNode>(root) {
    
    override fun insertTopDown(index: Int, instance: LayoutNode) {
        // 自顶向下插入,不做任何操作
    }
    
    override fun insertBottomUp(index: Int, instance: LayoutNode) {
        // 自底向上插入,将节点添加到当前节点的子节点
        current.insertAt(index, instance)
    }
    
    override fun remove(index: Int, count: Int) {
        current.removeAt(index, count)
    }
    
    override fun move(from: Int, to: Int, count: Int) {
        current.move(from, to, count)
    }
    
    override fun onClear() {
        root.removeAll()
    }
}
💡 为什么使用 Bottom-Up 插入?

Compose UI 使用 insertBottomUp 策略,因为 LayoutNode 的测量/布局依赖于子节点。自底向上插入可以确保在父节点附加时,子节点已经准备就绪,减少不必要的重新测量。

4.3 Node 发射过程

当遇到 Layout 类型的 Composable(如 Box, Column, Text)时,Composer 会发射节点:

// Layout Composable 的简化实现
@Composable
inline fun Layout(
    content: @Composable () -> Unit,
    measurePolicy: MeasurePolicy
) {
    val materialized = currentComposer.materialize(content)
    
    ReusableComposeNode<LayoutNode, Applier<Any>>(
        factory = { LayoutNode() },
        update = {
            set(measurePolicy) { this.measurePolicy = it }
        },
        content = materialized
    )
}

// ReusableComposeNode 内部
@Composable
inline fun <T, E> ReusableComposeNode(
    noinline factory: () -> T,
    update: @DisallowComposableCalls Updater<T>.() -> Unit,
    content: @Composable () -> Unit
) {
    if (currentComposer.inserting) {
        // 初始组合:创建新节点
        currentComposer.createNode(factory)
    } else {
        // 重组:复用现有节点
        currentComposer.useNode()
    }
    
    // 更新节点属性
    Updater<T>(currentComposer).update()
    
    // 递归处理子内容
    content()
}

V. 组合执行流程

5.1 初始组合流程

// 1. 创建 Composition
val composition = Composition(
    applier = UiApplier(rootLayoutNode),
    parent = recomposer
)

// 2. 设置内容
composition.setContent {
    MyApp()
}

// 内部执行过程:
private fun doCompose(content: @Composable () -> Unit) {
    // 1. 开始写入 Slot Table
    slotTable.write { writer ->
        composer.composeContent(content)
    }
    
    // 2. 应用变更到 Node Tree
    applier.applyChanges()
    
    // 3. 执行 Side Effects
    runSideEffects()
}

5.2 重组流程

State 变更触发重组: 1. 状态变更 count.value = 1 ──→ Snapshot 系统检测到写入 ↓ 2. 标记失效 找到读取该状态的 RecomposeScope 将其添加到 Composition.invalidations ↓ 3. 调度重组 Recomposer 在下一帧调度 recompose() ↓ 4. 执行重组 Composer 重新执行失效的 @Composable ├── 跳过未变更的部分 (skipping = true) ├── 更新变更的部分 └── 写入 Slot Table 差异 ↓ 5. 应用差异 Applier 将变更应用到 Node Tree ├── 更新节点属性 ├── 插入/移除节点 └── 触发重新测量/布局
internal fun recompose(): Boolean {
    if (invalidations.isEmpty()) return false
    
    // 收集需要重组的作用域
    val toRecompose = invalidations.toList()
    invalidations.clear()
    
    // 对每个作用域执行重组
    slotTable.write { writer ->
        for (scope in toRecompose) {
            // 定位到该作用域在 Slot Table 中的位置
            composer.recompose(scope)
        }
    }
    
    // 应用变更
    val changes = composer.collectChanges()
    applier.applyChanges(changes)
    
    return true
}

VI. Remember 与 State 的存储

6.1 remember 的实现

@Composable
inline fun <T> remember(calculation: @DisallowComposableCalls () -> T): T {
    return currentComposer.cache(
        invalid = false,  // 永远有效,不重新计算
        calculation
    )
}

@Composable
inline fun <T> remember(
    key1: Any?,
    calculation: @DisallowComposableCalls () -> T
): T {
    return currentComposer.cache(
        invalid = currentComposer.changed(key1),  // key 变化时重新计算
        calculation
    )
}

// Composer.cache 实现
fun <T> cache(invalid: Boolean, block: () -> T): T {
    return if (inserting || invalid) {
        // 初始组合或失效时,计算并存储
        val value = block()
        updateCachedValue(value)
        value
    } else {
        // 重组时,从 Slot Table 读取
        readCachedValue()
    }
}

6.2 State 与重组的关联

@Composable
fun Counter() {
    // 1. remember 将 MutableState 存入 Slot Table
    var count by remember { mutableStateOf(0) }
    
    // 2. 读取 count.value 时,Snapshot 系统记录:
    //    "当前 RecomposeScope 依赖于这个 State"
    Text("Count: $count")  // 触发 count.value 读取
    
    // 3. 点击时,count.value 被写入
    //    Snapshot 系统通知所有依赖的 RecomposeScope
    Button(onClick = { count++ }) {
        Text("+")
    }
}
🔍 RecomposeScope 追踪机制

每个 @Composable 函数执行时会创建 RecomposeScope。当函数内部读取 State 时,Snapshot 系统会将这个 State 与 RecomposeScope 关联。State 变化时,所有关联的 Scope 都会被标记为失效。

VII. 自定义 Applier:超越 UI

7.1 Compose 的通用性

Compose Runtime 并不局限于 UI。通过自定义 Applier,你可以将 Compose 用于任何树形结构:

// 自定义节点类型
class StringNode(var value: String = "") {
    val children = mutableListOf<StringNode>()
}

// 自定义 Applier
class StringApplier(root: StringNode) : AbstractApplier<StringNode>(root) {
    override fun insertBottomUp(index: Int, instance: StringNode) {
        current.children.add(index, instance)
    }
    
    override fun remove(index: Int, count: Int) {
        current.children.subList(index, index + count).clear()
    }
    
    override fun move(from: Int, to: Int, count: Int) {
        val dest = if (from > to) to else to - count
        val moved = current.children.subList(from, from + count).toList()
        current.children.subList(from, from + count).clear()
        current.children.addAll(dest, moved)
    }
    
    override fun onClear() {
        root.children.clear()
    }
}

// 使用自定义 Composition
fun renderToString(content: @Composable () -> Unit): String {
    val root = StringNode()
    val composition = Composition(
        applier = StringApplier(root),
        parent = Recomposer(Dispatchers.Main)
    )
    composition.setContent(content)
    return root.buildString()
}

7.2 真实案例

VIII. 性能优化视角

8.1 跳过 (Skipping) 机制

@Composable
fun Parent() {
    var count by remember { mutableStateOf(0) }
    
    Column {
        Text("Count: $count")  // 会重组
        ExpensiveChild()        // 可以跳过!
        Button(onClick = { count++ }) {
            Text("+")
        }
    }
}

@Composable
fun ExpensiveChild() {
    // 没有读取任何变化的状态
    // Composer.skipping = true,整个函数被跳过
    Text("I'm expensive but stable")
}

8.2 智能重组范围

Parent 重组时的执行情况: @Composable fun Parent() │ ├── count 状态变化 → 需要重组 │ └── Column ├── Text("Count: $count") → 需要重组 (读取 count) │ ├── ExpensiveChild() → 跳过! (参数未变化) │ └── Button └── Text("+") → 跳过! (参数未变化)

IX. 总结

🎯 核心要点

理解 Compose Runtime 的工作原理,能够帮助你: