Compose 原理深入详解
更新: 5/15/2026 字数: 0 字 时长: 0 分钟
1. Compose 编译器做了什么
1.1 @Composable 的本质
@Composable 不是普通注解,它会改变函数的类型签名。Compose 编译器插件在编译期对每个 @Composable 函数做以下变换:
// 你写的代码
@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)
}
}关键变换:
- 注入
$composer参数:Composer 是重组的执行引擎,管理 Slot Table 的读写 - 注入
$changed参数:位掩码,跟踪每个参数是否发生变化(每个参数占 2 bit) - 生成 group key:基于源码文件路径 + 行号 + 列号的哈希值,唯一标识这个 Composable 在组合树中的位置
- 包裹 startGroup/endGroup:在 Slot Table 中标记这个 Composable 的范围
1.2 $changed 参数详解
每个参数用 2 bit 表示状态:
00 = Unknown(不确定是否变化,需要 equals 比较)
01 = Same(确定没变)
10 = Different(确定变了)
11 = Static(编译期常量,永远不变)// 调用时编译器会计算 $changed
@Composable
fun Parent() {
val name = remember { "World" }
// 编译器知道 name 来自 remember,标记为 Same
Greeting(name, $composer, 0b01) // Same
}这让 Compose 在很多情况下不需要调用 equals,直接通过位掩码判断是否可以跳过。
1.3 可跳过(Skippable)vs 可重启(Restartable)
// 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 无法确定内容是否变化,每次都要重组查看编译器报告:
# build.gradle.kts
composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_reports")
metricsDestination = layout.buildDirectory.dir("compose_metrics")
}
# 生成的报告会显示每个函数是否 skippable/restartable2. 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 重组时的流程:
- 将 gap 移到当前重组位置
- 遍历旧的 Slot Table,与新的组合结果对比
- 相同的 group 保留(跳过)
- 新增的 group 写入 gap
- 删除的 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/draw3. 重组机制
3.1 重组作用域(RecomposeScope)
每个 Restartable 的 Composable 函数对应一个 RecomposeScope。当函数内读取的 State 发生变化时,这个 Scope 被标记为 invalid,等待重组。
@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 函数被跳过需要满足:
- 函数是 Restartable 的(有 startRestartGroup)
- 所有参数都是稳定类型
- 所有参数的值与上次相同(通过 equals 或 $changed 位掩码判断)
稳定类型的定义:
- 基本类型(Int, Float, Boolean, String 等)
- 函数类型(lambda)—— 但有陷阱,见下文
- 标记了
@Stable或@Immutable的类 - 所有属性都是 val 且类型稳定的 data class
// ✅ 稳定:所有属性都是 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
// @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 导致不必要重组的原因
// ❌ 每次 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") }
}解决方案:
// ✅ 方案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 final4. Snapshot 系统
4.1 MutableState 的实现
// 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:
// 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 中执行,类似数据库的事务隔离:
// 重组开始
val snapshot = Snapshot.takeMutableSnapshot()
snapshot.enter {
// 在这个 Snapshot 中执行重组
// 读取的是 Snapshot 创建时的数据快照
// 写入的修改暂时不可见给其他线程
}
// 重组完成,应用修改
snapshot.apply() // 类似 commit
// 或者
snapshot.dispose() // 类似 rollback好处:
- 重组过程中,其他线程修改 State 不会影响当前重组
- 重组失败可以回滚
- 支持并发重组(不同 Scope 可以在不同线程重组)
5. 副作用 API 详解
5.1 LaunchedEffect
在 Composable 进入组合时启动协程,离开时自动取消。key 变化时重启。
@Composable
fun SearchScreen(query: String) {
var results by remember { mutableStateOf(emptyList<Item>()) }
// query 变化时,取消旧协程,启动新协程
LaunchedEffect(query) {
delay(300) // 防抖
results = api.search(query)
}
ItemList(results)
}源码原理:
@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):
@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
每次成功重组后执行(不是挂起函数,同步执行):
@Composable
fun Analytics(screenName: String) {
// 每次重组后同步执行
SideEffect {
analytics.setCurrentScreen(screenName)
}
}适用场景:将 Compose 状态同步到非 Compose 管理的对象。
5.4 derivedStateOf
将多个 State 合并为一个派生 State,只在结果变化时触发重组:
@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) }
}
}经典场景:列表滚动时判断是否显示"回到顶部"按钮:
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 变化时发射新值:
@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 缩小重组范围
// ❌ 整个函数都会重组
@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
// ❌ 在组合阶段读取,触发重组
@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 参数时的解决方案:
- 用
@Immutable标记数据类 - 用
kotlinx.collections.immutable的ImmutableList替代List - 将不稳定参数拆分为稳定的基本类型参数
