重组触发机制:重组条件、重组范围、重组粒度
原理概述
Compose 的重组(Recomposition)是框架在观察到的状态发生变化后,重新执行可组合函数以更新 UI 的过程。触发重组的核心条件是:可观察状态(State)发生改变,且该状态被当前组合范围内的可组合函数读取。Compose 通过快照系统(Snapshot System)追踪状态读取,并仅在必要的最小范围内重新执行代码。
重组触发条件
| 条件类型 | 具体场景 | 重组范围 |
|---|---|---|
| 状态变化 | mutableStateOf、StateFlow、LiveData 等可观察对象的值改变 |
所有读取该状态的 Composable 及调用链 |
| 重组回传 | 父 Composable 重组导致子 Composable 参数变化 | 直接受影响的子 Composable |
| 隐式触发 | 使用 remember 但 key 变化;derivedStateOf 依赖变化;LaunchedEffect key 变化 |
仅该 Composable 内部逻辑 |
| 非重组副作用 | SideEffect、DisposableEffect 的生命周期回调 |
不触发 UI 重组,仅执行副作用 |
重组范围与粒度
Compose 不会重组整个界面,而是以可组合函数为最小粒度进行“跳过”或“执行”。重组范围由状态读取的 读取作用域(Read Scope)决定:
- 函数级粒度:只要某个 Composable 内读取了某个状态,该 Composable 整体会被标记为“需要重组”。
- 跳过机制:参数未变的 Composable 如果被标记为
@NonRestartableComposable或参数稳定(Stable)则跳过重组。 - 最小化范围:通过
derivedStateOf或keys可进一步收窄重组范围。
优化策略
- 拆分细粒度 Composable:将频繁变化的部分拆成独立 Composable,避免父级大范围重组。
- 使用
remember缓存稳定对象:例如remember { mutableStateOf(...) }避免每次重组重新创建。 - 避免在重组中执行昂贵计算:移到
derivedStateOf或LaunchedEffect中。 - 标记稳定类型:对数据类使用
@Stable或@Immutable注解,帮助 Compose 跳过不必要的重组。 - 使用
key参数:在LazyColumn等列表组件中提供唯一 key,减少全量重组。
关键守则:重组 ≠ 重新绘制。重组只重建 UI 树结构,不一定会导致重绘。优化目标是减少重组次数,而非重绘次数。
代码实现示例
以下示例展示不同粒度的重组触发:
@Composable
fun CounterExample() {
var count by remember { mutableStateOf(0) }
// 每次 count 变化,整个 CounterExample 重组
Column {
Text("Count: $count")
// 独立子 Composable,仅在内部读取状态时重组
IncrementButton(onClick = { count++ })
}
}
@Composable
fun IncrementButton(onClick: () -> Unit) {
// 这里未读取任何状态,因此永远不会因 count 变化而重组
Button(onClick = onClick) {
Text("+1")
}
}
调试工具
- Compose Compiler Metrics:在 build.gradle 中配置
composeCompilerReports和composeCompilerMetrics,输出重组频率和稳定类型报告。 - Layout Inspector:Android Studio 内置工具,实时查看 Composable 重组次数。
- RecompositionCount:通过
Recomposer或自定义调试工具统计。
在 Gradle 中添加以下配置以生成 Compiler Metrics:
android {
composeOptions {
kotlinCompilerExtensionVersion = "1.5.4"
}
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
freeCompilerArgs += [
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
project.buildDir.absolutePath + "/compose-reports"
]
freeCompilerArgs += [
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
project.buildDir.absolutePath + "/compose-metrics"
]
}
}
常见错误
- 在非协程中直接修改状态:导致同步触发重组,可能引发卡顿。应使用
snapshotFlow或协程。 - 将可变对象(如 List)存入状态:修改对象内部属性不会触发重组。应使用不可变复制或
mutableStateListOf。 - 不必要的全局状态:将多个不相关状态放在同一个 ViewModel 中,导致局部变化引发大范围重组。
性能验证方法
- 使用 Compose Compiler Metrics 报告:检查每个 Composable 的
restartable和skippable标记。 - 帧率监控:通过 Profile GPU Rendering 或 Systrace 观察帧时间。
- 手动添加日志:在 Composable 开头打印日志,观察调用次数。