4. Compose 编译器与运行时机制
Compose 的性能表现并不主要取决于“写得多不多”,而取决于“编译器如何理解你的状态读取与函数调用”。Compose 编译器在编译期把可组合函数改写成带有 Composer 参数的形式,并在运行时通过 Composer 记录每个重组作用域的状态依赖。理解这条链路,才能解释为什么同样的界面代码,有的只刷新一个文本,有的却整页重算。
一、重组作用域:编译器插入的“刷新边界”
重组作用域(Recomposition Scope)是编译器为可组合函数中“可能独立失效”的代码块建立的刷新单元。最典型的是内联 lambda:当你在 Column、Row、Box 的 content 中写代码,编译器会把它包装成一个可重启的 lambda,并为其分配一个独立的 RecomposeScope。当该 lambda 内读取的 State 发生变化时,只有这个 lambda 被标记为失效,而不是整个外层函数。
关键点在于:作用域由编译器按调用点划分,而不是按代码行划分。同一个函数被调用两次,会得到两个不同的作用域实例;循环中调用同一个可组合函数,每次迭代也会形成独立作用域。因此,把状态读取尽量下沉到小的 lambda 或子可组合函数中,是缩小刷新范围最直接的手段。
二、智能重组:只重跑“读过该状态”的作用域
智能重组(Smart Recomposition)指运行时只重新执行那些真正读取了已变更状态的作用域。Compose 运行时维护一张“状态对象 → 作用域”的依赖表:作用域首次执行时,每次读取 State.value 都会把当前作用域登记为该状态的观察者;状态写入时,只把登记过的作用域标记为失效,并在下一帧统一调度执行。
这带来两个工程结论:其一,读取位置决定刷新范围,把 state.value 读在父函数里,父函数就会整体失效;其二,写入时机影响帧节奏,同一帧内多次写入会被合并,但跨帧写入会触发多次重组。下面这张图展示了从状态写入到作用域重跑、再到跳过判断的完整信号链。
三、跳过优化:为什么“没变”的函数可以完全不跑
跳过优化是 Compose 性能的核心红利。当某个可组合函数被标记为失效、即将重新执行前,运行时会先做一次判断:如果该函数的所有参数与上次执行时相等,且参数类型都被推断为稳定,那么这次执行可以直接跳过,复用上一次生成的 UI 节点。跳过意味着不进入函数体、不重新读取状态、不产生新的节点操作。
跳过的前提是“参数可比”。Compose 对参数使用 equals 比较,因此 data class 天然友好,而普通类若未重写 equals,每次新建实例都会被视为“不相等”,导致跳过失败。这也是为什么在列表项中传入 data class 比传入可变对象更高效。
四、稳定性推断:编译器如何判断一个类型能不能跳过
稳定性(Stability)是编译器对类型的静态推断结果,含义是:该类型的公开属性若发生变化,Compose 能通过 equals 感知到。推断规则大致如下:
val修饰的基本类型、String、函数类型:Stable。data class且所有字段均为 Stable:Stable。- 含
var字段、或字段类型为List/Set/Map等接口类型:Unstable(接口实现可能可变)。 - 使用
@Stable或@Immutable可手动声明,编译器会信任该声明。
当参数类型被推断为 Unstable 时,即使值没变,编译器也无法保证 equals 可靠,于是该函数无法被跳过,每次父作用域失效都会连带重跑。这是很多“看起来只改了一个字段却整页刷新”问题的根因。
五、性能影响与工程实践
把上述机制落到工程上,可以形成几条可操作的判断依据:
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| 只改一个文本,整页重组 | 状态读取位置过高 | 把 state.value 下沉到最小 lambda |
| 列表滚动卡顿 | item 参数为 Unstable 类型 | 改用 data class 或加 @Immutable |
| 函数每次都被重跑 | 参数未重写 equals | 使用 data class 或实现 equals |
| 跳过失效 | 传入了 List 接口类型 | 改用 ImmutableList 或 @Stable 包装 |
build.gradle.kts 中开启 composeCompiler { metricsDestination = layout.buildDirectory.dir("compose-metrics") },构建后查看生成的 *-composables.txt 与 *-classes.txt。前者会标注每个可组合函数是否 restartable、是否 skippable,后者会列出被推断为 unstable 的类。若某个函数显示 skippable=false,就应检查其参数类型是否稳定。
@Stable。该注解是承诺:若类型内部存在可变状态却标注 Stable,Compose 会跳过本应发生的重组,导致界面不更新。稳定性标注必须与真实的可变性语义一致。
总结来看,Compose 的性能模型是一条清晰的因果链:状态写入触发依赖表查询,只失效登记过的作用域;作用域重跑前先做跳过判断,判断依赖参数相等性与类型稳定性;稳定性由编译器静态推断,也可由开发者显式声明。把状态读取下沉、把参数类型做稳、把不稳定类型收敛到最小范围,就能让智能重组真正发挥作用,而不是退化成整页重算。