Recomposition — 它是什么,状态改变时 UI 的重建

作者: IT Sectr 发布日期: 2026-06-27 阅读时间: 7 分钟

Recomposition 是 Jetpack Compose 的一种机制,当数据发生变化时自动重建用户界面的部分,无需手动更新 View 元素。当 Composable 函数所依赖的状态变量改变值时,Compose 只会重启该函数,而保持 UI 树的其余部分不变。根据 Google Android Developers, 2026 的数据,正确理解 Recomposition 可以减少 40–60% 的不必要重绘。

要点

  • Recomposition — 在输入数据或状态改变时重新运行 Composable 函数
  • Skipping — 跳过参数未更改的函数(通过 equals 比较)
  • Stability 决定 Compose 是否可以跳过函数 — 稳定类型被正确比较
  • Smart Recomposition 只重启最小函数集,而不是整个树
  • 重组 不保证重绘 — Layout 和 Drawing 可以跳过阶段

Jetpack Compose 中的 Recomposition 是什么

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 的组件重组。

kotlin
@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 会在每次更改时重启列表的所有元素,这在大型列表上会导致明显的性能损失。

kotlin
// 优化后的结构:稳定部分单独提取
@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)
}

Compose 中的 Skipping 和 Stability

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 中监控重组

为了监控重组,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 { } 中。

kotlin
// 不好:每次父重组时创建新的 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),否则应用程序将变慢。

为什么在 State 没有改变的情况下函数会重组?

原因是来自父函数的参数变化。父函数(出于自身原因)重启并传递新值。为避免这种情况,请检查参数的 stability 并使用 remember 来稳定 lambda 和计算值。

能否禁用特定函数的重组?

没有直接禁用,但存在通过 readInComposition 的强制跳过 — State 在函数体外读取,不注册依赖。请谨慎使用:函数不会响应更改,可能导致 UI 过时。

哪个更昂贵:Composition 还是 Recomposition?

Composition 更昂贵,因为它从头创建所有槽位和树节点。Recomposition 重用现有槽位并仅更新它们的内容。实际上,屏幕的 Composition 需要 2–10 ms,而单个元素的重组需要 0.1–1 ms。

总结

  • Recomposition — 在 State 或参数更改时选择性重启 Composable 函数
  • Snapshot system 跟踪函数对 State 的依赖关系并计划重组
  • Skipping 仅对具有稳定参数的函数可能(@Stable 或 immutable)
  • 三个触发器 用于重组:State 更改、参数更改、CompositionLocal 更改
  • List<T> 被认为不稳定 — 使用不可变集合实现正确的 skipping
  • Layout Inspector 显示每个函数的重组计数器
  • 建议:将 UI 的稳定部分提取到单独的函数中,并为 lambda 使用 remember

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读