跳到主要内容

Compose 性能优化

问题

Jetpack Compose 有哪些性能优化手段?如何减少不必要的重组?

答案

1. 理解重组范围

Compose 编译器会以 可重启的 Composable 函数 为最小重组单元。当 State 变化时,只有读取了该 State 的最小作用域会被重新执行:

@Composable
fun UserCard(name: String, avatar: String) {
Column {
// 当 name 变化时,只有 Text 所在的 lambda 重组
Text(name)
// avatar 不变,Image 不会重组
Image(painter = rememberAsyncImagePainter(avatar), contentDescription = null)
}
}

2. 稳定性注解

Compose 编译器会判断参数的稳定性,不稳定的参数会导致每次父组件重组时子组件也被重组:

// ❌ 不稳定:MutableList 是可变的
data class UserState(
val name: String,
val tags: MutableList<String> // 不稳定,导致无法跳过重组
)

// ✅ 稳定:所有字段都是不可变且稳定的类型
data class UserState(
val name: String,
val tags: List<String> // List 是稳定的(只读接口)
)

// 手动标记为稳定(当你确定类的内容不会改变时)
@Stable
class Counter(val value: Int)

// 手动标记为不可变
@Immutable
data class UserProfile(val id: Long, val name: String)
稳定性规则
  • 原始类型(Int, String 等):稳定
  • 不可变数据类(所有字段 val 且为稳定类型):稳定
  • MutableList, MutableMap 等可变集合:不稳定
  • 含 var 字段的类:不稳定
  • 第三方库的类(编译器无法判断):不稳定

3. derivedStateOf

将多个 State 合并为派生状态,避免中间状态引起的多余重组:

@Composable
fun FilteredList(items: List<String>, query: String) {
// ❌ 每次 items 或 query 变化都会计算
// val filtered = items.filter { it.contains(query) }

// ✅ 只有计算结果变化时才触发重组
val filtered by remember(items, query) {
derivedStateOf { items.filter { it.contains(query, ignoreCase = true) } }
}

LazyColumn {
items(filtered) { Text(it) }
}
}

4. 延迟读取(Defer reads)

将 State 的读取点从 Composition 阶段推迟到 Layout/Drawing 阶段:

// ❌ 滚动时每一帧都触发重组
@Composable
fun Header(scrollOffset: Int) {
Text(
modifier = Modifier.offset(y = (-scrollOffset).dp) // 在 Composition 阶段读取
)
}

// ✅ 在 Layout 阶段读取,跳过重组
@Composable
fun Header(scrollOffsetProvider: () -> Int) {
Text(
modifier = Modifier.offset {
IntOffset(0, -scrollOffsetProvider()) // 在 Layout 阶段读取
}
)
}

越靠后的阶段读取 State,跳过的阶段越多,性能越好。

5. key 的正确使用

LazyColumn 中提供稳定的 key,避免不必要的项重组:

LazyColumn {
items(
items = users,
key = { it.id } // ✅ 使用唯一且稳定的 key
) { user ->
UserItem(user)
}
}

6. remember 缓存计算

@Composable
fun ExpensiveScreen(data: List<Int>) {
// ✅ 只有 data 变化时才重新计算
val sorted = remember(data) {
data.sorted() // 昂贵的排序操作
}
}

7. 编译器报告

通过 Compose 编译器指标检查稳定性问题:

build.gradle.kts
composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_compiler")
metricsDestination = layout.buildDirectory.dir("compose_compiler")
}

生成的报告会标注哪些类不稳定、哪些函数无法跳过重组。


常见面试问题

Q1: @Stable@Immutable 的区别?

答案

特性@Stable@Immutable
含义属性可变,但变化时会通知 Compose一旦创建,所有属性永不改变
约束较宽松更严格(@Immutable 隐含 @Stable
等价性equals 返回 true 则永远返回 true同上
典型场景MutableState 内部就是 @Stable数据类、配置值

Q2: Compose 为什么可以跳过重组?原理是什么?

答案

Compose 编译器插件会为每个 Composable 函数自动插入比较代码。在重组时,编译器生成的代码会检查:

  1. 所有参数是否与上次调用相同(通过 equals 比较)
  2. 如果所有参数都相同且类型都是稳定的,则跳过该函数的执行

如果参数类型不稳定,编译器无法保证 equals 的可靠性,就不会插入跳过逻辑。

Q3: 列表滚动卡顿怎么优化?

答案

  1. 使用 LazyColumn/LazyRow 而非 Column + forEach
  2. 提供稳定的 keyitems(list, key = { it.id })
  3. 避免在 item 中做复杂计算:使用 remember 缓存
  4. 图片使用 Coil 的异步加载AsyncImage
  5. 确保 item 的 Composable 可以跳过重组:参数都用稳定类型
  6. 使用 contentType 标记不同类型的 item,优化回收复用

Q4: 如何排查 Compose 的性能问题?

答案

  1. Layout Inspector:可视化重组次数(Recomposition Count)
  2. Compose 编译器报告:检查稳定性、可跳过性
  3. Systrace / Perfettoandroidx.compose 标签,定位耗时 Composition
  4. Release 模式测试:Debug 模式下 Compose 的性能不具参考价值

相关链接