Composable 是描述,不是 View 工厂
它可以按任意顺序、任意频率重跑,甚至本次结果被丢弃;所以函数体必须像一次可撤销的草稿。
一个 Composable 重组了,下面哪件事一定发生?
- 对应 Android View 被销毁重建
- 它下面每个子 Composable 都重新执行
- 函数重新求值,但运行时可跳过未变化的子树
- 所有 Modifier 都触发布局和绘制
Composition 是对调用位置的记忆
第一次执行 Composable 叫初始组合,运行时记录调用结构与状态槽位。状态变化时,读取该状态的重组作用域被标记,函数再次执行,新的描述与旧 Composition 对照后应用最小变更。它不是把 XML 换成 Kotlin,也不是每次创建一整棵 View。
状态槽位通常靠调用位置识别。条件分支改变调用顺序、列表在头部插入元素,都会改变「这里是谁」。key(id) 与 Lazy 列表的 key 给内容稳定身份,让 remember 状态、动画与副作用跟着业务对象走,而不是跟着位置走。
重组不是性能失败
重组是正常机制,目标不是把计数器降到零。真正要问的是:重组范围是否合理,函数体是否便宜,参数不变时能否跳过。Compose 编译器根据类型稳定性判断能否安全比较参数;稳定参数相等时,可跳过子调用。
稳定不等于所有字段都用 val。一个 val items: MutableList<T> 仍可能在运行时偷偷改变而不通知 Compose。也不要随手给类型贴 @Stable;这是你向编译器作出的合同,合同撒谎会导致 UI 不更新。先使用真正不可变的 UI 模型、快照状态容器或不可变集合,再看性能证据。
读状态的阶段决定重做多少
Compose 大致经历组合、布局、绘制三阶段。在 Composable 函数体读取状态,变化会触发重组;在布局 lambda 里读,可能只重做布局;在绘制 lambda 里读,可能只重绘。把滚动偏移这种每帧变化的值延后到 graphicsLayer 或布局阶段,能绕开不必要的组合。
@Composable
fun FadingToolbar(scroll: () -> Float) {
Box(
Modifier.graphicsLayer {
alpha = 1f - scroll().coerceIn(0f, 1f)
}
)
}
remember 缓存昂贵计算;derivedStateOf 把高频输入压成低频派生状态,例如「首项是否已离开」。两者都不是默认必加,只有计算真昂贵或输出变化频率显著更低时才值回复杂度。
用 SideEffect 只记录成功提交的重组次数。点一次按钮,父级状态变化;稳定且参数不变的子项可能被跳过:
Counter committed: 1 StaticTitle committed: 1 Counter committed: 2 StaticTitle may be skipped
@Composable
fun RecomposeProbe() {
var count by remember { mutableIntStateOf(0) }
SideEffect { Log.d("Probe", "Counter committed: ${count + 1}") }
Column {
StaticTitle(title = "Messages")
Text("Count: $count")
Button(onClick = { count++ }) { Text("Add") }
}
}
@Composable
fun StaticTitle(title: String) {
SideEffect { Log.d("Probe", "StaticTitle committed") }
Text(title)
}在 Android Studio 的 Empty Activity 模板运行,用 Layout Inspector 打开重组计数。Debug 日志只作学习探针;真实性能结论要在 release 构建和 Macrobenchmark 中测。
消息列表每次收到新消息时,若没有稳定 key,插到顶部可能让大量 item 的位置身份改变,图片加载与本地展开状态跟错对象。给 key 之后,Compose 能把「同一条消息换了位置」和「出现了一条新消息」区分开。
「重组次数越少越好,看到重组就加 remember 和 @Stable。」这会把可读性换成没有测量依据的复杂度,甚至因错误稳定合同漏更新。
重组应便宜、可跳过、无副作用。先修错误的状态范围和身份,再用工具确认瓶颈。
答案是第 3 项。重组意味着描述函数重新求值,不等于 View 树重建,也不保证所有孩子执行。运行时能跳过输入未变且可安全比较的子树,布局与绘制也可能独立跳过。
这一章的一句话
把 Composable 写成可反复重放、没有隐藏副作用的描述,Compose 才有资格替你跳过工作。
下一章只问一个决定整个页面质量的问题:草稿、列表滚动、筛选条件和服务端消息,各自应该活过哪一种死亡?四个答案分别落在三个状态容器和一个持久层里。