Kotlin Lambda、函数引用与 Compose:先核验,再缓存
2026-09-30 更正: Lambda 分配和 Compose 跳过行为取决于编译配置及调用位置。旧版对单例、SAM 分配和“所有回调都要手动 remember”的表述过于绝对,本次删除这些结论,并修复缓存示例遗漏回调 key 的问题。本文不声称已经取得分配次数或帧耗时收益。
先分开三个问题
同一个操作可以写成函数引用,也可以写成 lambda:
fun label(id: Long) = "Item $id"
val reference: (Long) -> String = ::label
val expression: (Long) -> String = { id -> label(id) }
但需要分别回答:
- 是否执行了预期操作,捕获的接收者和值是否正确?
- 当前 Kotlin 编译器与后端,在这个具体调用位置生成了什么?
- 改写回调是否真的影响了已经测量的界面交互?
函数类型相同,不代表对象身份相同。比较两个不同源码位置的表达式,也不能证明同一个位置重复执行时的行为。业务正确性不应依赖函数引用或 lambda 一定是单例。
用真实编译配置核验 JVM 产物
Kotlin 2.0 将普通 JVM lambda 的默认生成策略改为 invokedynamic,编译选项可以改变这一点,见 Kotlin 2.0 发布说明。不能据此推导所有 callable reference 都采用相同 lowering,也不能只看语法就断言运行时固定分配几个对象。
可以把最小函数保存为 Callbacks.kt,记录 kotlinc -version,再生成并检查产物:
kotlinc Callbacks.kt -jvm-target 17 -d callbacks-out
javap -c -p -classpath callbacks-out CallbacksKt
这里是排查步骤,不是本文实际生成的字节码输出。存在生成类时也要检查相关类。Android 项目还涉及 D8/R8 和 Compose 编译器转换,普通 JVM 输出只是证据的一部分。性能结论仍需要对象分配和帧耗时测量。
SAM 转换不等于固定的分配次数
函数式接口只有一个抽象方法,Kotlin 可以把 lambda 或 callable reference 适配成该接口。捕获内容、编译后端、优化和调用位置都会影响结果,仅凭表达式不能证明“每次执行一定 new 一个对象”。
绑定引用 controller::open 依赖 controller。如果 controller 会变化,却永久缓存旧回调,即使减少了某些分配,也会产生正确性问题。先明确生命周期与身份需求,再决定是否缓存。
Compose:lambda 稳定性与记忆化是两件事
Compose 将 lambda 类型视为稳定类型;某次传入的 lambda 值是否改变,是另一个问题。Kotlin 2.0.20 起默认开启 Strong Skipping,Composable 内的 lambda 会按捕获值自动记忆化。因此,不能断言嵌套的 { onUserClick(user) } 总会重新创建回调并强制子组件重组。
先使用清楚的写法:
@Composable
fun UserList(users: List<String>, onUserClick: (String) -> Unit) {
LazyColumn {
items(users, key = { it }) { user ->
UserCard(user = user, onClick = { onUserClick(user) })
}
}
}
这个示例要求作为 key 的 user 字符串唯一;真实应用应使用 UI model 的稳定唯一 ID。代码是片段,需要常规 Compose 导入和项目自己的 UserCard 实现。
加手动缓存之前,先检查编译报告和运行行为。确实需要为特定配置手动缓存时,必须包含两个可能变化的捕获值:
val onClick = remember(user, onUserClick) {
{ onUserClick(user) }
}
UserCard(user = user, onClick = onClick)
旧版的 remember(user) 遗漏了 onUserClick:user 不变但父级更换处理函数时,缓存 lambda 可能仍调用旧函数。应单独验证这个场景。普通 remember 的 key 比较,也不能与 Strong Skipping 生成的捕获值比较一概等同。
一组小而具体的回归检查
- 输入不变时重组父组件,查看相关 release 报告
- 保持 user 不变但替换点击回调,点击后必须调用新回调
- 插入或重排用户,确认行身份和已记忆状态仍正确
- 使用相同设备和构建变体测量交互,再解释计数变化
可以用本地 Compose 报告阅读器整理标签,再沿着重组排查指南定位。skippable 表示编译器给出的资格,不代表零重组或更快的帧。