Recomposition 是 Jetpack Compose 的一种机制,当数据发生变化时自动重建用户界面的部分,无需手动更新 View 元素。当 Composable 函数所依赖的状态变量改变值时,Compose 只会重启该函数,而保持 UI 树的其余部分不变。根据 Google Android Developers, 2026 的数据,正确理解 Recomposition 可以减少 40–60% 的不必要重绘。
要点
Recomposition 是已经参与过 Composition 的 Composable 函数以新的参数值或状态值重新执行。重组的主要目标是将 UI 树与当前数据同步,而无需从头重建整个界面。与一次性发生的 Composition 不同,Recomposition 在屏幕的生命周期内可以启动数百次。
Recomposition 基于 smart invalidation 原则工作:Compose 跟踪每个 Composable 函数读取哪些 State 对象,并仅标记那些依赖关系已更改的函数进行重启。这是通过 snapshot 系统实现的,它记录执行期间的所有 State 读取操作,并由 Composer 将这些依赖关系映射到特定的函数。
重要的是要理解:重组并不意味着立即重绘屏幕。Compose 分三个阶段工作:Composition(构建 UI 描述)、Layout(计算尺寸和位置)和 Drawing(在画布上绘制)。如果重组后元素的尺寸和位置没有改变,Layout 阶段可以跳过。如果外观没有改变 — Drawing 被跳过。这种三阶段架构确保了每次 UI 更新的最小成本。
重组有三个主要触发器。第一个 — 在 Composable 函数体中读取的 State 对象发生变化。当 mutableStateOf 或 derivedStateOf 改变其值时,所有在前一次组合中注册读取此 State 的函数都被标记为要重启。
第二个触发器 — 从父函数调用 Composable 函数时 参数的变化。如果父函数传递了新值(例如文本或数字发生了变化),即使子函数内部不读取 State,它也会被重启。Compose 通过 equals 比较参数的新旧值,如果相等 — 函数可以被跳过。
第三个触发器 — 通过 CompositionLocalProvider 更改 CompositionLocal。所有通过 .current 读取 CompositionLocal 的函数在 provider 更改时都会重启。MaterialTheme 使用此机制:主题更改(亮/暗)会导致所有读取 MaterialTheme.colorScheme 的组件重组。
@Composable
fun RecompositionDemo() {
var counter by remember { mutableStateOf(0) }
var text by remember { mutableStateOf("Hello") }
Column {
Text("计数器:$counter") // 计数器改变时重组
Text("消息:$text") // 文本改变时重组
Button(onClick = { counter++ }) {
Text("+1")
}
Button(onClick = { text = "世界" }) {
Text("更改文本")
}
}
}
按下 +1 按钮会更改 counter,这只导致第一行 Text 和 Column 本身的重组。显示 text 的第二行 Text 不会重启。这种隔离 — snapshot 系统的结果:每个 Composable 函数只知道它读取过的 State 对象。
重组的优化从正确选择数据结构开始。使用不可变集合(listOf, mapOf)代替可变集合(mutableListOf)。Compose 通过 equals 比较参数,如果集合发生了变化但 equals 返回 true — 函数不会重启。对于可变集合,使用 SnapshotStateList,它实现了元素级别的正确变化跟踪。
第二个技巧 — 将 UI 的稳定部分提取到单独的 Composable 函数中。如果屏幕的一部分不依赖于频繁变化的状态,将其提取到带参数的单独函数中。当重组发生时,稳定函数接收相同的参数,Compose 比较它们并跳过执行。这比在参数部分变化的大函数中重启此部分更有利。
第三个技巧 — LazyColumn 中的键。始终为 LazyColumn、LazyGrid 和其他惰性容器中的 item 指定 key。键允许 Compose 在列表更改时识别元素:添加、删除或重新排序。没有键时,Compose 会在每次更改时重启列表的所有元素,这在大型列表上会导致明显的性能损失。
// 优化后的结构:稳定部分单独提取
@Composable
fun OptimizedScreen(items: List<Item>) {
Column {
Header() // 不依赖于项目 — 没有重组
Spacer(modifier = Modifier.height(8.dp))
LazyColumn {
items(items, key = { it.id }) { item ->
ItemRow(item = item) // 仅对更改的项目进行重组
}
}
}
}
@Composable
fun Header() {
Text("项目列表", style = MaterialTheme.typography.headlineMedium)
}
@Composable
fun ItemRow(item: Item) {
Text(item.title)
}
Skipping 是一种机制,当 Composable 函数的所有参数都没有改变时,Compose 跳过其执行。为了使 skipping 正常工作,参数类型必须是稳定的(stable)。Kotlin 编译器标记为稳定的类型:原始类型(Int, Float, Boolean)、String、lambda 函数,以及所有字段都是稳定且为 val 的类。
Stability 是可以添加到自定义数据类的 @Stable 或 @Immutable 注解。如果类包含可变字段(var),编译器将其视为不稳定,Compose 不能跳过具有此类参数的函数。对于包含 var 的类,如果保证将通过 snapshot 系统发送更改通知,请使用 @Stable。
可以通过编译器标志检查 Stability:-P "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=. /reports"。它会生成一个报告,列出所有 Composable 函数及其参数并标明 stability。如果参数不稳定 — 该函数的 skipping 不可能,并且会在每次父级重组时重启。
| 类型 | 稳定性 | Skipping |
|---|---|---|
| Int, Float, Boolean | 稳定 | 是 |
| String | 稳定 | 是 |
| Lambda | 稳定 | 是 |
| 具有 val 字段的 data class | 稳定 | 是 |
| 具有 var 字段的 data class | 不稳定 | 否 |
| List<String> | 不稳定 | 否 |
请注意:List<String> 被认为是不稳定的,因为它是一个接口,而不是具体实现。使用 Kotlin Collections Immutable 库中的 immutableListOf() 或将列表包装在 @Stable 类中。Lambda 始终是稳定的,因为它的 equals 只比较引用,并且在调用位置创建新的 lambda 时,父函数也会被重启。
为了监控重组,Android Studio 提供了 Layout Inspector,带有 Compose Recomposition Counts 模式。在此模式下,每个 Composable 函数显示重组的次数和重启原因。这可以快速找到过于频繁重组的函数并确定根本原因 — 不稳定的参数或不必要的 State 依赖。
附加工具:Compose Metrics(通过 instrumentation 测试收集统计信息)和 Recomposition Timer(测量每个函数的执行时间)。Google 建议在性能分析阶段启用这些工具,并在发布版本中禁用,因为它们每次重组会增加最多 20% 的开销。
在分析重组时,查找 unnecessary recomposition 模式:函数被重启,尽管其输出 UI 不应该改变。常见原因是在没有 remember 的情况下使用 lambda,每次都会创建一个新的 lambda 对象,Compose 认为参数已更改。解决方案:将 lambda 包装在带有固定捕获的 remember { } 中。
// 不好:每次父重组时创建新的 lambda
@Composable
fun Parent() {
Child(onClick = { doSomething() }) // 每次都创建新的 lambda
}
// 好:remember 稳定了 lambda
@Composable
fun Parent() {
val onClick = remember { { doSomething() } }
Child(onClick = onClick) // 相同的引用
}
常见问题
不,重组只是 Composition 阶段。之后执行 Layout 和 Drawing。如果重组后元素的尺寸和位置没有改变,Layout 和 Drawing 可以完全跳过,从而节省 GPU 资源。
在动画中,重组每秒最多可以启动 120 次(120fps)。对于正常交互 — 每秒 10–60 次。重要的是每次重组要符合帧预算(8–16 ms),否则应用程序将变慢。
原因是来自父函数的参数变化。父函数(出于自身原因)重启并传递新值。为避免这种情况,请检查参数的 stability 并使用 remember 来稳定 lambda 和计算值。
没有直接禁用,但存在通过 readInComposition 的强制跳过 — State 在函数体外读取,不注册依赖。请谨慎使用:函数不会响应更改,可能导致 UI 过时。
Composition 更昂贵,因为它从头创建所有槽位和树节点。Recomposition 重用现有槽位并仅更新它们的内容。实际上,屏幕的 Composition 需要 2–10 ms,而单个元素的重组需要 0.1–1 ms。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。