ART与Compose:Compose Runtime与ART交互、Slot表与重组、Compose编译器插件
说实话,Jetpack Compose 刚出来那会儿,我第一反应是:这不就是个 UI 框架吗?跟 ART 能有多大关系?
后来深入一看,才发现我错了。Compose 的底层,其实跟 ART 的交互非常深。你想想看,一个声明式 UI 框架,要在 Android 上跑得流畅,靠什么?靠的就是 ART 的运行时能力,以及编译器插件的配合。
今天我们就来拆解这三块:Compose Runtime 怎么跟 ART 打交道、Slot 表怎么支撑重组、以及Compose 编译器插件到底干了什么。
一、Compose Runtime 与 ART 的交互
Compose Runtime 并不是一个独立的虚拟机。它跑在 ART 之上,本质上是一套 Kotlin 编写的运行时库。但它的核心机制——重组(Recomposition)——却深度依赖 ART 的某些特性。
1.1 重组与 ART 的栈帧
重组是什么?说白了,就是当状态变化时,Compose 重新执行某些 Composable 函数。这个过程不是在编译期完成的,而是在运行时,由 ART 执行 Kotlin 字节码。
我遇到过一个问题:某个 Composable 函数里有个大循环,状态一变,整个界面卡顿。后来发现,重组时 ART 会创建大量临时栈帧,GC 压力陡增。
这里的关键点是:Compose 的 @Composable 函数会被编译器插件改写,插入大量对 Compose Runtime 的调用。这些调用最终会落到 ART 的堆栈管理上。
核心交互点:
- Composable 函数的调用栈由 ART 管理
- 重组时,ART 需要快速创建/销毁栈帧
- Slot 表的数据存储在 ART 堆上
1.2 类型安全与 ART 的泛型
Compose 的 State
我记得有一次,一个同事把 MutableState> 写成了 MutableState
,结果运行时直接 ClassCastException。嗯,这就是类型擦除的坑。
二、Slot 表与重组
Slot 表是 Compose Runtime 最核心的数据结构之一。没有它,重组根本没法高效进行。
2.1 Slot 表是什么?
你可以把 Slot 表想象成一个位置感知的存储数组。每个 Composable 函数在编译时,都会被分配一组 Slot。这些 Slot 用来存储:
- 函数的输入参数
- 内部状态(remember 的值)
- 子 Composable 的位置信息
为什么需要 Slot 表?因为重组时,Compose 需要知道:“上次这个位置存了什么?” 如果没有 Slot 表,每次重组都得重新计算所有状态,那性能就崩了。
2.2 Slot 表的结构
Slot 表本质上是一个 Object[] 数组。每个 Composable 函数对应一组连续的 Slot。编译器插件会生成索引常量,用来访问这些 Slot。
// 编译后的伪代码
fun MyComposable($composer: Composer, $changed: Int) {
// Slot 0: 存储输入参数
// Slot 1: 存储 remember 的状态
$composer.startRestartGroup()
if ($composer.changed(message)) {
// 重组逻辑
}
Text(message)
$composer.endRestartGroup()
}
我个人习惯把 Slot 表叫做「Compose 的寄存器文件」。它跟 CPU 的寄存器很像——位置固定,访问快速。
2.3 重组时 Slot 表怎么工作?
当状态变化时,Compose Runtime 会遍历 Slot 表,找到受影响的 Composable 函数的位置。然后只重组那些位置对应的函数。
这里有个关键优化:如果某个 Composable 函数的输入没变,Compose 会跳过它的重组。这就是为什么 key() 和 remember 能提升性能——它们帮 Slot 表更准确地判断是否需要重组。
避坑指南:我曾经在列表中使用大量不带 key 的 Composable,结果每次状态变化,整个列表都重组。加上 key 之后,重组范围缩小到只有变化的项。性能提升非常明显。
三、Compose 编译器插件
Compose 编译器插件是 Kotlin 编译器的一个扩展。它做的事情,说白了就是:把普通的 Kotlin 函数,改造成支持重组的 Composable 函数。
3.1 插件做了什么?
编译器插件在编译期干了几件大事:
- 插入 Composer 参数:每个 Composable 函数都会被添加一个
$composer: Composer参数 - 生成 Slot 表访问代码:为每个 Composable 函数生成 Slot 索引
- 插入重组检查逻辑:在函数开头插入
$composer.changed()调用 - 生成组标记:用
startRestartGroup()和endRestartGroup()包裹函数体
3.2 编译后的代码长什么样?
我们写一个简单的 Composable:
@Composable
fun Greeting(name: String) {
Text("Hello $name")
}
经过编译器插件处理后,它大概变成这样:
fun Greisting($composer: Composer, $changed: Int) {
$composer.startRestartGroup()
if ($composer.changed(name)) {
Text("Hello $name", $composer, 0b0001)
}
$composer.endRestartGroup()
}
注意那个 $changed 参数。它是一个位掩码,用来告诉 Compose Runtime:哪些参数可能变了。这个机制能进一步减少不必要的重组检查。
3.3 插件与 ART 的配合
编译器插件生成的代码,最终由 ART 执行。这里有个微妙的地方:插件的优化程度,直接影响 ART 的执行效率。
比如,插件会尽量生成 inline 函数,减少函数调用开销。ART 对 inline 函数有专门的优化路径,能避免栈帧创建。
注意:Compose 编译器插件是 Kotlin 编译器插件,不是 ART 插件。它工作在编译期,而 ART 工作在运行时。两者通过字节码和运行时库(Compose Runtime)进行交互。
四、知识体系总览
下面这张图,我把 ART、Compose Runtime、Slot 表、编译器插件的关系画了出来。你看一眼就能明白整体架构。
五、总结与个人体会
ART 与 Compose 的交互,其实是一个编译期 + 运行时的协同设计。编译器插件负责生成高效的字节码,Compose Runtime 负责在 ART 上管理重组,Slot 表则充当了状态与位置的桥梁。
我做了这么多年 Android 系统开发,最大的感受是:理解底层机制,能帮你写出更高效的代码。比如,知道 Slot 表的工作原理后,你就会主动使用 key() 和 remember,而不是盲目堆代码。
嗯,今天就聊到这里。如果你在实际项目中遇到过 Compose 性能问题,不妨从 Slot 表和编译器插件入手分析——往往能发现意想不到的优化空间。
一句话总结:Compose 的流畅运行,离不开 ART 的运行时能力、Slot 表的高效重组、以及编译器插件的编译期优化。三者缺一不可。