深入理解Jetpack Compose key() {} 方法
Jetpack Compose 中 key() 的作用与性能优化解析
在 Jetpack Compose 中,key() 是一个非常重要但容易被忽视的 API。
很多开发者知道:
“列表里最好加 key”
但却不清楚:
- 它到底解决了什么问题?
- 不加会发生什么?
- 它是如何影响 recomposition 的?
- 它是否真的优化性能?
这篇文章会从 机制 → 问题 → 原理 → 优化效果 → 实战建议 全面讲清楚。
一、Compose 如何判断 UI 的“身份”?
Compose 在重组(Recomposition)时,需要判断:
“这个 Composable 是之前那个吗?”
默认情况下,它使用的是:
调用位置(Call Position)
也就是说:
if (代码位置没变) {
认为是同一个节点
}
二、问题:动态列表会出错
假设有如下代码:
for (item in list) {
Text(item.name)
}
如果 list 发生了排序变化:
list = list.sortedBy { it.price }
Compose 会认为:
- 第 0 个位置还是 Text
- 第 1 个位置还是 Text
它不知道:
这些 item 已经换人了
这就会导致:
- remember 状态错位
- 动画异常
- 选中状态跑错对象
- UI 重建异常
三、key() 的核心作用
key(item.id) {
Text(item.name)
}
加入 key 之后:
Compose 不再通过“位置”判断身份,而是通过:
你提供的 key
对比
| 无 key | 有 key |
|---|---|
| 通过位置判断身份 | 通过 key 判断身份 |
| 列表 reorder 会错位 | 顺序变化也能正确匹配 |
| remember 可能串数据 | 状态跟随 item |
| 可能重建 subtree | 精准复用节点 |
四、它解决的核心问题
1️⃣ 避免状态错位
错误示例:
for (stock in stocks) {
var expanded by remember { mutableStateOf(false) }
}
当 stocks 重排时:
- expanded 会跟“位置”走
- A 股票的状态跑到 B 股票上
正确做法:
for (stock in stocks) {
key(stock.code) {
var expanded by remember { mutableStateOf(false) }
}
}
现在:
expanded 会跟 stock.code 走
2️⃣ 减少错误重建
没有 key:
- Compose 认为每个位置都变了
- 销毁 + 重建节点
有 key:
- 只移动节点
- 不销毁 subtree
- 状态正确复用
3️⃣ 列表 reorder 性能更稳定
对于:
- LazyColumn
- LazyRow
- HorizontalPager
- 动态排序列表
key 能避免大量不必要的重建。
⚠️ 注意:
key 并不是“加速 UI”,
它是“避免错误的重建”。
五、和 LazyColumn key 的区别
Lazy API 也有 key:
LazyColumn {
items(list, key = { it.id }) {
...
}
}
区别:
| key() | LazyColumn key |
|---|---|
| 普通 Compose 作用域 | Lazy 内部 diff 用 |
| 手动控制 identity | Lazy 自动 diff |
| 影响 remember | 影响 item 复用 |
本质机制相似,但作用域不同。
六、底层原理(进阶理解)
Compose 内部维护一个 SlotTable。
默认:
identity = callPosition
使用 key 后:
identity = callPosition + keyHash
这会影响:
- remember slot
- 状态恢复
- node 复用
- subtree 移动
七、什么时候必须使用 key()
✅ 动态列表
✅ 列表会 reorder
✅ item 内部有 remember
✅ item 内有动画
✅ Pager 页面切换
✅ 可折叠列表
❌ 静态 UI
❌ 结构固定且顺序不变
八、key() vs remember(key)
很多人会混淆:
key(item.id) { ... }
remember(item.id) { ... }
区别:
| API | 作用 |
|---|---|
| key() | 改变节点身份 |
| remember(key) | 控制值是否重算 |
它们解决的是完全不同的问题。
九、一句话总结
key() 的本质是告诉 Compose:
这个节点是谁,而不是它在第几个位置。
它的价值不是“提升性能”,
而是:
- 防止状态错位
- 防止错误重组
- 保证结构稳定性
- 减少 subtree 重建
十、结语
如果你在开发:
- 股票/Trading 列表
- 可折叠列表
- 动态排序列表
- HorizontalPager
- 带动画的 LazyColumn
那么:
key 不是优化选项,而是结构安全保证。
当你理解 identity 的概念后,你就真正理解了 Compose 的重组机制。