Skip to content

Compose 原理深入详解

更新: 5/15/2026 字数: 0 字 时长: 0 分钟

1. Compose 编译器做了什么

1.1 @Composable 的本质

@Composable 不是普通注解,它会改变函数的类型签名。Compose 编译器插件在编译期对每个 @Composable 函数做以下变换:

kotlin
// 你写的代码
@Composable
fun Greeting(name: String) {
    Text("Hello $name")
}

// 编译器变换后(简化)
fun Greeting(name: String, $composer: Composer, $changed: Int) {
    $composer.startRestartGroup(2104126067) // group key,基于源码位置的哈希

    if ($changed and 0b0001 == 0 && $composer.getSkipping()) {
        // 参数没变,跳过重组
        $composer.skipToGroupEnd()
    } else {
        // 执行函数体
        Text("Hello $name", $composer, ...)
    }

    $composer.endRestartGroup()?.updateScope { composer, _ ->
        // 注册重组回调:当需要重组时,重新调用自己
        Greeting(name, composer, $changed or 0b0001)
    }
}

关键变换:

  1. 注入 $composer 参数:Composer 是重组的执行引擎,管理 Slot Table 的读写
  2. 注入 $changed 参数:位掩码,跟踪每个参数是否发生变化(每个参数占 2 bit)
  3. 生成 group key:基于源码文件路径 + 行号 + 列号的哈希值,唯一标识这个 Composable 在组合树中的位置
  4. 包裹 startGroup/endGroup:在 Slot Table 中标记这个 Composable 的范围

1.2 $changed 参数详解

每个参数用 2 bit 表示状态:

00 = Unknown(不确定是否变化,需要 equals 比较)
01 = Same(确定没变)
10 = Different(确定变了)
11 = Static(编译期常量,永远不变)
kotlin
// 调用时编译器会计算 $changed
@Composable
fun Parent() {
    val name = remember { "World" }
    // 编译器知道 name 来自 remember,标记为 Same
    Greeting(name, $composer, 0b01) // Same
}

这让 Compose 在很多情况下不需要调用 equals,直接通过位掩码判断是否可以跳过。

1.3 可跳过(Skippable)vs 可重启(Restartable)

kotlin
// Restartable + Skippable(理想状态)
// 所有参数都是稳定类型(基本类型、String、@Stable/@Immutable 标记的类)
@Composable
fun UserCard(name: String, age: Int) { ... }

// Restartable + NOT Skippable
// 参数包含不稳定类型(普通 class、List、Map 等)
@Composable
fun UserList(users: List<User>) { ... }
// List 不是稳定类型,Compose 无法确定内容是否变化,每次都要重组

查看编译器报告:

bash
# build.gradle.kts
composeCompiler {
    reportsDestination = layout.buildDirectory.dir("compose_reports")
    metricsDestination = layout.buildDirectory.dir("compose_metrics")
}
# 生成的报告会显示每个函数是否 skippable/restartable

2. Slot Table 与 Gap Buffer

2.1 Slot Table 是什么

Slot Table 是 Compose 存储组合树状态的核心数据结构。它用两个线性数组存储整棵树:

groups: IntArray    → 存储 Group 的元数据(key, size, parent, data slot 范围)
slots: Array<Any?> → 存储实际数据(State 值、remember 的值、CompositionLocal 等)
组合树:
  Column
  ├── Text("Hello")
  └── Button(onClick)
      └── Text("Click")

Slot Table(线性化):
groups: [Column | Text | Button | Text]
slots: ["Hello" | onClick_lambda | "Click"]

为什么用线性数组而不是树结构?

  • 内存紧凑,缓存友好
  • 遍历快(顺序访问)
  • 适合 Gap Buffer 优化

2.2 Gap Buffer 算法

Gap Buffer 是文本编辑器常用的数据结构。核心思想:在数组中维护一个"间隙"(gap),插入/删除操作只需要移动 gap 到目标位置。

初始状态(首次组合后):
[A][B][C][D][E]  gap在末尾

需要在 B 和 C 之间插入 X:
1. 移动 gap 到 B 后面:
   [A][B][___gap___][C][D][E]
2. 在 gap 位置写入 X:
   [A][B][X][__gap__][C][D][E]

需要删除 D:
1. 移动 gap 到 D 位置:
   [A][B][X][C][___gap___][E]
   (D 被 gap 覆盖,等于删除)

Compose 重组时的流程:

  1. 将 gap 移到当前重组位置
  2. 遍历旧的 Slot Table,与新的组合结果对比
  3. 相同的 group 保留(跳过)
  4. 新增的 group 写入 gap
  5. 删除的 group 被 gap 覆盖

2.3 首次组合 vs 重组

首次组合(Composition)

Composer.inserting = true
→ 所有 Composable 都执行
→ 数据写入 Slot Table
→ 生成 LayoutNode 树
→ 交给 Compose UI 进行 measure/layout/draw

重组(Recomposition)

Composer.inserting = false
→ 遍历 Slot Table,对比新旧数据
→ 参数没变的 Composable 跳过(skipToGroupEnd)
→ 参数变了的 Composable 重新执行,更新 Slot Table
→ 只有变化的 LayoutNode 需要重新 measure/layout/draw

3. 重组机制

3.1 重组作用域(RecomposeScope)

每个 Restartable 的 Composable 函数对应一个 RecomposeScope。当函数内读取的 State 发生变化时,这个 Scope 被标记为 invalid,等待重组。

kotlin
@Composable
fun Counter() {  // ← 这是一个 RecomposeScope
    var count by remember { mutableStateOf(0) }  // 读取 State
    Text("Count: $count")  // ← 这也是一个 RecomposeScope
    Button(onClick = { count++ }) {
        Text("Add")  // ← 这也是一个 RecomposeScope
    }
}

count 变化时,只有读取了 count 的 Scope 需要重组。Text("Add") 没有读取 count,不会重组。

3.2 State 变化如何触发重组

完整链路:

1. count++
   → MutableState.setValue()
   → Snapshot.writeObserver 被通知

2. writeObserver 记录:这个 State 变了
   → 查找所有读取过这个 State 的 RecomposeScope
   → 将这些 Scope 标记为 invalid

3. Recomposer 收到通知
   → 在下一帧(通过 Choreographer)执行重组
   → 只重新执行 invalid 的 Scope 对应的 Composable 函数

4. 重组执行
   → Composer 遍历 Slot Table
   → 到达 invalid Scope 时,重新执行函数体
   → 对比新旧参数,更新 Slot Table
   → 生成新的 LayoutNode 变更

3.3 智能跳过的条件

Composable 函数被跳过需要满足:

  1. 函数是 Restartable 的(有 startRestartGroup)
  2. 所有参数都是稳定类型
  3. 所有参数的值与上次相同(通过 equals 或 $changed 位掩码判断)

稳定类型的定义:

  • 基本类型(Int, Float, Boolean, String 等)
  • 函数类型(lambda)—— 但有陷阱,见下文
  • 标记了 @Stable@Immutable 的类
  • 所有属性都是 val 且类型稳定的 data class
kotlin
// ✅ 稳定:所有属性都是 val + 基本类型
data class User(val id: Int, val name: String)

// ❌ 不稳定:有 var 属性
data class User(val id: Int, var name: String)

// ❌ 不稳定:List 不是稳定类型(可能被外部修改)
data class UserGroup(val users: List<User>)

// ✅ 手动标记稳定(你保证 List 不会被修改)
@Immutable
data class UserGroup(val users: List<User>)

3.4 @Stable vs @Immutable

kotlin
// @Immutable:所有属性永远不变(创建后不会修改)
// Compose 可以完全信任 equals 结果
@Immutable
data class Color(val r: Int, val g: Int, val b: Int)

// @Stable:属性可能变化,但变化时会通知 Compose(通过 MutableState)
// 适用于包含 MutableState 的类
@Stable
class CounterState {
    var count by mutableStateOf(0)  // 变化时自动通知
}

区别:

  • @Immutable:更强的保证,对象创建后不会变。Compose 可以更激进地跳过
  • @Stable:对象可能变,但变化是可观察的。Compose 仍然可以跳过(因为变化会触发重组)

3.5 Lambda 导致不必要重组的原因

kotlin
// ❌ 每次 Parent 重组,都创建新的 lambda 实例
@Composable
fun Parent() {
    var count by remember { mutableStateOf(0) }

    // 这个 lambda 捕获了 count,每次 count 变化都会创建新实例
    ChildButton(onClick = { println(count) })
}

@Composable
fun ChildButton(onClick: () -> Unit) {
    // onClick 每次都是新对象,equals 返回 false
    // 即使 ChildButton 内部没有变化,也会重组
    Button(onClick = onClick) { Text("Click") }
}

解决方案:

kotlin
// ✅ 方案1:用 remember 缓存 lambda
val onClick = remember { { println(count) } }
// 注意:这样 count 被捕获的是初始值,不会更新

// ✅ 方案2:用 rememberUpdatedState
val currentCount by rememberUpdatedState(count)
val onClick = remember { { println(currentCount) } }

// ✅ 方案3:不捕获变量,通过参数传递
ChildButton(count = count, onClick = { c -> println(c) })

// ✅ 方案4:Compose 编译器优化
// 如果 lambda 没有捕获任何可变变量,编译器会自动将其提升为单例
val onClick = { println("static") }  // 编译器优化为 static final

4. Snapshot 系统

4.1 MutableState 的实现

kotlin
// mutableStateOf 返回的是 SnapshotMutableStateImpl
fun <T> mutableStateOf(value: T): MutableState<T> =
    SnapshotMutableStateImpl(value, StructuralEqualityPolicy())

class SnapshotMutableStateImpl<T>(
    value: T,
    val policy: SnapshotMutationPolicy<T>
) : StateObject, MutableState<T> {

    // 实际值存储在 StateRecord 链表中(支持多版本)
    private var next: StateStateRecord<T> = StateStateRecord(value)

    override var value: T
        get() {
            // 读取时通知 readObserver
            val snapshot = Snapshot.current
            snapshot.readObserver?.invoke(this)  // ← 关键:记录谁读了这个 State
            return next.readable(this, snapshot).value
        }
        set(value) {
            // 写入时通知 writeObserver
            val snapshot = Snapshot.current
            val record = next.writable(this, snapshot)
            if (!policy.equivalent(record.value, value)) {
                record.value = value
                snapshot.writeObserver?.invoke(this)  // ← 关键:通知 State 变了
            }
        }
}

4.2 读取追踪

当 Composable 函数执行时,Composer 设置了 readObserver:

kotlin
// Composer 在执行重组时
Snapshot.observe(
    readObserver = { state ->
        // 记录:当前 RecomposeScope 读取了这个 State
        currentRecomposeScope.recordRead(state)
    },
    writeObserver = { state ->
        // 记录:这个 State 被修改了
        // 找到所有读取过它的 Scope,标记为 invalid
    }
) {
    // 在这个 block 中执行 Composable 函数
    composable()
}

这就是 Compose 的"自动依赖追踪":你不需要手动声明依赖关系,只要在 Composable 中读取了某个 State,Compose 就自动知道这个 Composable 依赖这个 State。

4.3 Snapshot 隔离

每次重组在自己的 Snapshot 中执行,类似数据库的事务隔离:

kotlin
// 重组开始
val snapshot = Snapshot.takeMutableSnapshot()

snapshot.enter {
    // 在这个 Snapshot 中执行重组
    // 读取的是 Snapshot 创建时的数据快照
    // 写入的修改暂时不可见给其他线程
}

// 重组完成,应用修改
snapshot.apply()  // 类似 commit
// 或者
snapshot.dispose()  // 类似 rollback

好处:

  • 重组过程中,其他线程修改 State 不会影响当前重组
  • 重组失败可以回滚
  • 支持并发重组(不同 Scope 可以在不同线程重组)

5. 副作用 API 详解

5.1 LaunchedEffect

在 Composable 进入组合时启动协程,离开时自动取消。key 变化时重启。

kotlin
@Composable
fun SearchScreen(query: String) {
    var results by remember { mutableStateOf(emptyList<Item>()) }

    // query 变化时,取消旧协程,启动新协程
    LaunchedEffect(query) {
        delay(300)  // 防抖
        results = api.search(query)
    }

    ItemList(results)
}

源码原理:

kotlin
@Composable
fun LaunchedEffect(key1: Any?, block: suspend CoroutineScope.() -> Unit) {
    val applyContext = currentComposer.applyCoroutineContext
    // remember 一个 LaunchedEffectImpl
    remember(key1) {
        LaunchedEffectImpl(applyContext, block)
    }
}

// LaunchedEffectImpl 实现了 RememberObserver
class LaunchedEffectImpl : RememberObserver {
    private var job: Job? = null

    override fun onRemembered() {
        // 进入组合时启动协程
        job = scope.launch(context, block = task)
    }

    override fun onForgotten() {
        // 离开组合时取消协程
        job?.cancel()
    }

    override fun onAbandoned() {
        job?.cancel()
    }
}

5.2 DisposableEffect

需要清理资源的副作用(类似 onDestroy):

kotlin
@Composable
fun LocationTracker() {
    val context = LocalContext.current

    DisposableEffect(Unit) {
        val listener = LocationListener { location -> /* ... */ }
        val manager = context.getSystemService<LocationManager>()
        manager.requestLocationUpdates(GPS_PROVIDER, 0, 0f, listener)

        onDispose {
            // 离开组合时清理
            manager.removeUpdates(listener)
        }
    }
}

与 LaunchedEffect 的区别:

  • LaunchedEffect:异步操作(网络请求、延迟任务)
  • DisposableEffect:需要配对的注册/注销操作(监听器、回调)

5.3 SideEffect

每次成功重组后执行(不是挂起函数,同步执行):

kotlin
@Composable
fun Analytics(screenName: String) {
    // 每次重组后同步执行
    SideEffect {
        analytics.setCurrentScreen(screenName)
    }
}

适用场景:将 Compose 状态同步到非 Compose 管理的对象。

5.4 derivedStateOf

将多个 State 合并为一个派生 State,只在结果变化时触发重组:

kotlin
@Composable
fun FilteredList(items: List<Item>, query: String) {
    // ❌ 每次 items 或 query 变化都重组,即使过滤结果没变
    val filtered = items.filter { it.name.contains(query) }

    // ✅ 只在过滤结果变化时重组
    val filtered by remember(items, query) {
        derivedStateOf { items.filter { it.name.contains(query) } }
    }

    LazyColumn {
        items(filtered) { item -> ItemRow(item) }
    }
}

经典场景:列表滚动时判断是否显示"回到顶部"按钮:

kotlin
val listState = rememberLazyListState()

// 只在 "是否可见" 这个布尔值变化时重组,而不是每次滚动都重组
val showButton by remember {
    derivedStateOf { listState.firstVisibleItemIndex > 0 }
}

if (showButton) {
    FloatingActionButton(onClick = { /* scroll to top */ }) { ... }
}

5.5 snapshotFlow

将 Compose State 转为 Flow,在 State 变化时发射新值:

kotlin
@Composable
fun ScrollLogger(listState: LazyListState) {
    LaunchedEffect(listState) {
        snapshotFlow { listState.firstVisibleItemIndex }
            .distinctUntilChanged()
            .collect { index ->
                analytics.logScroll(index)
            }
    }
}

原理:snapshotFlow 内部创建 Snapshot,在 block 中读取 State 时记录依赖,State 变化时重新执行 block 并发射新值。

6. Compose 性能优化实战

6.1 缩小重组范围

kotlin
// ❌ 整个函数都会重组
@Composable
fun Screen() {
    var count by remember { mutableStateOf(0) }
    ExpensiveHeader()  // 不依赖 count,但每次都重组
    Text("Count: $count")
    ExpensiveFooter()  // 不依赖 count,但每次都重组
}

// ✅ 将读取 State 的部分提取为独立 Composable
@Composable
fun Screen() {
    var count by remember { mutableStateOf(0) }
    ExpensiveHeader()  // 不重组
    CountText(count)   // 只有这个重组
    ExpensiveFooter()  // 不重组
}

@Composable
fun CountText(count: Int) {
    Text("Count: $count")
}

6.2 延迟读取 State

kotlin
// ❌ 在组合阶段读取,触发重组
@Composable
fun AnimatedBox() {
    val offset by animateFloatAsState(targetValue = 100f)
    Box(Modifier.offset(x = offset.dp))  // offset 每帧变化,每帧都重组
}

// ✅ 在布局/绘制阶段读取,跳过重组
@Composable
fun AnimatedBox() {
    val offset by animateFloatAsState(targetValue = 100f)
    Box(Modifier.offset { IntOffset(offset.toInt(), 0) })  // lambda 版本,在布局阶段读取
    // 或
    Box(Modifier.graphicsLayer { translationX = offset })  // 在绘制阶段读取
}

Compose 的三个阶段:

Composition(组合)→ Layout(布局)→ Drawing(绘制)

State 在越晚的阶段读取,跳过的工作越多。

6.3 使用 Compose Compiler Metrics

# 生成的报告示例(module_name-composables.txt)
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun UserCard(
  stable name: String
  stable age: Int
)

restartable scheme("[androidx.compose.ui.UiComposable]") fun UserList(
  unstable users: List<User>  ← 不稳定参数,无法跳过
)

看到 unstable 参数时的解决方案:

  1. @Immutable 标记数据类
  2. kotlinx.collections.immutableImmutableList 替代 List
  3. 将不稳定参数拆分为稳定的基本类型参数