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 函数自动插入比较代码。在重组时,编译器生成的代码会检查:
- 所有参数是否与上次调用相同(通过
equals比较) - 如果所有参数都相同且类型都是稳定的,则跳过该函数的执行
如果参数类型不稳定,编译器无法保证 equals 的可靠性,就不会插入跳过逻辑。
Q3: 列表滚动卡顿怎么优化?
答案:
- 使用
LazyColumn/LazyRow而非Column + forEach - 提供稳定的 key:
items(list, key = { it.id }) - 避免在 item 中做复杂计算:使用
remember缓存 - 图片使用 Coil 的异步加载:
AsyncImage - 确保 item 的 Composable 可以跳过重组:参数都用稳定类型
- 使用
contentType标记不同类型的 item,优化回收复用
Q4: 如何排查 Compose 的性能问题?
答案:
- Layout Inspector:可视化重组次数(Recomposition Count)
- Compose 编译器报告:检查稳定性、可跳过性
- Systrace / Perfetto:
androidx.compose标签,定位耗时 Composition - Release 模式测试:Debug 模式下 Compose 的性能不具参考价值