27、性能优化:remember、derivedStateOf、key 的使用,避免重组
性能优化这个话题,说实话,在 Compose 里绕不开。
我见过不少项目,功能都跑通了,界面也好看,但一滑动列表就卡成 PPT。问题出在哪?说白了,就是重组次数太多了。Compose 的声明式 UI 虽然爽,但如果你不管住重组,它就会像脱缰的野马一样到处跑。
今天我们就来聊聊三个核心工具:remember、derivedStateOf、key。用好它们,你的界面就能从「勉强能用」变成「丝般顺滑」。
重组到底是个啥?
先简单过一下概念。Compose 里,只要状态变了,跟这个状态相关的 Composable 函数就会重新执行。这个过程就叫重组。
重组本身不是坏事,它是 Compose 的核心理念。但问题是,如果重组范围太大、频率太高,UI 线程就会被塞满,掉帧就来了。
嗯,这里要注意:重组不是整个界面重绘,而是只重组那些「依赖了变化状态」的组件。但如果你代码写得糙,一个按钮的点击就能让整个页面重跑一遍,那就尴尬了。
remember:把值「记住」
remember 是 Compose 里最基础的缓存工具。它的作用很简单:在重组时,如果 key 没变,就不重新计算,直接拿上次的结果。
我举个例子。假设你有一个函数,每次调用都生成一个随机数:
@Composable
fun RandomNumberDisplay() {
val number = Random.nextInt(100)
Text("随机数:$number")
}
这段代码有什么问题?每次重组,Random.nextInt 都会重新执行,数字一直在变。这显然不是我们想要的。
加上 remember 就好了:
@Composable
fun RandomNumberDisplay() {
val number = remember { Random.nextInt(100) }
Text("随机数:$number")
}
这样,只有首次进入组合时才会生成随机数,后续重组直接复用。
remember 包一层。比如创建列表适配器、格式化日期、解析 JSON 等。
remember 的 key 参数
remember 还可以传一个 key。当 key 变化时,缓存会失效,重新计算。
我在项目中遇到过这样一个场景:一个详情页,根据传入的 userId 加载用户信息。如果 userId 变了,必须重新加载,不能复用旧数据。
@Composable
fun UserProfile(userId: String) {
val userInfo = remember(key1 = userId) {
fetchUserFromNetwork(userId)
}
// 展示用户信息
}
这里 key 就是 userId。当用户切换时,userId 变了,remember 发现 key 不匹配,就会重新执行 fetchUserFromNetwork。
remember 没区别。
derivedStateOf:派生状态
这个工具很多人容易忽略,但它非常实用。
derivedStateOf 的作用是:根据一个或多个状态,计算出一个新的值。而且它只在「真正需要」的时候才重新计算。
你想想看,如果你有一个列表,里面有一万个条目,你只想统计其中「已完成」的数量。如果每次列表变化都重新遍历一遍,那性能就炸了。
用 derivedStateOf 可以这样写:
@Composable
fun TaskList(tasks: List<Task>) {
val completedCount = remember {
derivedStateOf {
tasks.count { it.isCompleted }
}
}
Text("已完成:${completedCount.value}")
}
这里 derivedStateOf 会监听 tasks 的变化。只有当 tasks 列表本身发生变化时,才会重新计算 completedCount。如果只是界面重组但数据没变,它不会做无用功。
derivedStateOf 适合做「从现有状态推导出新状态」的场景,比如过滤、排序、统计。
key:帮 Compose 识别「谁是谁」
这个 key 不是 remember 的那个 key,而是 Composable 的 key 参数。
Compose 通过位置和类型来识别组件。但如果列表中的元素位置变了,或者你用了 animateContentSize 这类动画,Compose 可能会搞混,导致不该重组的重组了,或者动画效果错乱。
举个例子。一个可拖拽排序的列表,如果你不给每个条目加 key,拖拽后 Compose 会认为「位置 0 的条目还是原来那个」,结果状态全乱了。
@Composable
fun SortableList(items: List<Item>) {
LazyColumn {
items(items, key = { it.id }) { item ->
ItemRow(item)
}
}
}
加上 key = { it.id } 后,Compose 就能准确识别每个条目。即使位置变了,它也知道「哦,这个条目只是挪了个位置,内容没变,不用重组」。这能省下大量不必要的重组开销。
知识体系总览
下面这张图帮你理清这三个工具的关系:
避免重组的实战技巧
光知道工具还不够,得知道什么时候用。我总结几个实战场景:
1. 把「不变的内容」抽成独立 Composable
如果一个 Composable 里既有变化的部分,又有不变的部分,就把不变的部分抽出来。这样变化只影响小范围,不变的部分不会跟着重组。
@Composable
fun ExpensiveHeader() {
// 这里很重,但很少变化
Image(...)
Text("固定标题")
}
@Composable
fun DynamicContent(count: Int) {
Text("当前计数:$count")
}
2. lambda 表达式要小心
每次重组,lambda 都会创建新对象。如果 lambda 作为参数传给子组件,子组件就会因为「参数变了」而跟着重组。
解决办法:用 remember 包一下,或者用 derivedStateOf 把计算结果传下去。
// 不好的写法
Button(onClick = { viewModel.onClick() })
// 好的写法
val onClick = remember { { viewModel.onClick() } }
Button(onClick = onClick)
3. 列表用 LazyColumn + key
这个前面说过了,但值得再强调一遍。LazyColumn 本身已经做了按需渲染,但如果你不加 key,它就无法准确判断哪些条目需要重组。尤其是列表有增删改操作时,key 是必须的。
总结
性能优化这件事,说白了就是「少干活」。Compose 已经帮我们做了很多,但如果你不注意,它也会做很多无用功。
remember 帮你缓存计算结果,derivedStateOf 帮你按需派生状态,key 帮你精准识别组件。这三个工具用好了,你的 Compose 应用就能跑得又快又稳。
嗯,今天就聊到这。记住一句话:重组不是敌人,无意义的重组才是。
公众号:蓝海资料掘金营,微信 deep3321