25、性能优化:重组优化、稳定性注解、Key使用、懒加载优化
性能优化这个话题,说实话,是每个 Compose 开发者绕不开的坎。我记得刚用 Compose 做第一个商业项目时,列表稍微复杂一点就开始卡顿,滑动起来掉帧严重。当时我一度怀疑是不是 Compose 本身有问题,后来才发现——嗯,是我自己没用好。
这一章,我就把我在项目中踩过的坑、总结的经验,一次性讲清楚。核心就四个关键词:重组优化、稳定性注解、Key 使用、懒加载优化。
重组到底是个啥?
先说说重组。Compose 的 UI 是函数式的,状态变了,UI 自动更新。这个更新过程就叫重组。但问题来了——重组不是只更新变化的部分吗?理论上是的,但实际没那么简单。
我举个例子:
@Composable
fun MyScreen() {
var count by remember { mutableStateOf(0) }
Column {
Text("点击次数: $count")
Button(onClick = { count++ }) {
Text("点击")
}
HeavyComposable()
}
}
@Composable
fun HeavyComposable() {
// 模拟耗时操作
Thread.sleep(100)
Text("我是重型组件")
}
你猜怎么着?每次点击按钮,HeavyComposable 也会跟着重组。明明它跟 count 没有任何关系!这就是典型的重组范围过大问题。
核心原则:重组的最小单位是函数。如果一个 Composable 函数读取了某个状态,当这个状态变化时,整个函数都会重组。
所以,优化重组的第一步就是——把不相关的代码拆出去。把 HeavyComposable 单独抽成一个函数,它就不会被 count 的变化影响了。
稳定性注解:@Stable 和 @Immutable
接下来聊稳定性注解。这个知识点,说实话,很多人容易忽略。但它在性能优化里非常关键。
Compose 编译器会分析你的数据类型是否“稳定”。稳定的意思是:如果两个实例的 equals 返回 true,那么它们的内容一定相同。Compose 就可以放心地跳过重组。
默认情况下,基本类型、String、Lambda 表达式是稳定的。但自定义的 data class 呢?
data class User(val name: String, val age: Int)
这个 User 类,Compose 编译器会认为它是稳定的吗?答案是:不一定。如果 User 的所有属性都是稳定的,那它就是稳定的。但如果某个属性是 List<String> 呢?List 本身是不稳定的,因为它是接口,编译器不知道它的 equals 实现。
这时候就需要手动加注解了:
@Immutable
data class User(val name: String, val age: Int, val tags: List<String>)
加了 @Immutable 后,Compose 就知道这个类永远不会变,可以放心跳过重组。但注意——如果你骗了编译器,比如实际上你修改了 tags 的内容但没创建新对象,那 UI 就不会更新了。
我的习惯:所有作为 Composable 参数的数据类,我都加上 @Immutable 或 @Stable。除非它确实会变,那就用 @Stable。
@Stable 和 @Immutable 的区别很简单:@Immutable 表示对象永远不会变;@Stable 表示对象可以变,但 Compose 能感知到变化(比如通过 mutableStateOf)。
Key 的正确使用姿势
Key 这个东西,在 LazyColumn 里特别重要。我曾经遇到过一个 bug:列表数据更新后,某个 item 的动画效果错乱了。排查了半天,发现是 Key 没设置。
看这段代码:
@Composable
fun UserList(users: List<User>) {
LazyColumn {
items(users) { user ->
UserItem(user)
}
}
}
这样写有什么问题?当 users 列表发生变化时(比如某个元素位置变了),Compose 会通过 索引 来识别每个 item。但索引是不可靠的——如果第一个元素被删了,原来第二个元素就变成了第一个,它的状态(比如滚动位置、动画)会被错误地复用。
正确的做法是:
LazyColumn {
items(users, key = { it.id }) { user ->
UserItem(user)
}
}
用 id 作为 key,Compose 就能精确地追踪每个 item。即使列表顺序变了,每个 item 的状态也不会乱。
我曾经踩过的坑:用 hashCode() 作为 key。结果两个不同的对象 hashCode 冲突了,导致 UI 错乱。后来我统一用数据库里的唯一 ID,再也没出过问题。
懒加载优化:LazyColumn 的性能调优
LazyColumn 是 Compose 里最常用的懒加载组件。但用不好,照样卡。
我总结了几条实战经验:
- 避免在 item 里做耗时操作——每个 item 的 Composable 函数应该尽量轻量。如果要做图片加载、网络请求,用
LaunchedEffect或remember配合协程。 - 使用
contentType——如果你的列表里有多种类型的 item,告诉 Compose 它们类型不同,可以避免不必要的重组。 - 控制 item 的复杂度——一个 item 里嵌套太多层 Composable,重组时开销会很大。能拆就拆。
LazyColumn {
items(
items = users,
key = { it.id },
contentType = { "user" }
) { user ->
UserItem(user)
}
}
另外,remember 和 derivedStateOf 也是好帮手。比如你有一个列表,需要根据某个条件过滤:
val filteredUsers by remember(users, query) {
derivedStateOf {
users.filter { it.name.contains(query) }
}
}
这样只有当 users 或 query 变化时,才会重新计算过滤结果。否则直接复用上一次的结果。
知识体系总览
下面这张图,是我自己整理的重组优化知识体系。你可以把它当作一个检查清单:
总结一下
重组优化说白了就是一件事:让 Compose 少干活。你帮它把范围缩小、把数据标记清楚、把 Key 给对,它就能跑得飞快。
我个人习惯是:写每个 Composable 之前,先想想它会不会被不必要地重组。如果会,就拆。拆完之后加注解,再检查 Key。最后用 LazyColumn 时,把 contentType 和 derivedStateOf 用上。这一套组合拳打下来,性能基本不会出大问题。
嗯,今天就聊到这儿。记住一句话:性能优化不是玄学,是工程。