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 是个泛型类。在 ART 上,泛型是通过类型擦除实现的。但 Compose 编译器插件会在字节码中插入类型信息,确保重组时能正确识别状态类型。

我记得有一次,一个同事把 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 插件做了什么?

编译器插件在编译期干了几件大事:

  1. 插入 Composer 参数:每个 Composable 函数都会被添加一个 $composer: Composer 参数
  2. 生成 Slot 表访问代码:为每个 Composable 函数生成 Slot 索引
  3. 插入重组检查逻辑:在函数开头插入 $composer.changed() 调用
  4. 生成组标记:用 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 编译器插件 插入 Composer 参数 | 生成 Slot 索引 | 添加重组检查 | 生成组标记 输出:改写后的 Kotlin 字节码 运行时:Compose Runtime + Slot 表 Slot 表:Object[] 数组,存储状态和位置信息 重组机制:遍历 Slot 表,只重组受影响的 Composable 依赖:ART 堆内存、栈帧管理、类型系统 底层:ART 运行时 执行字节码 | 管理栈帧 | 垃圾回收 | 类型检查 提供:JIT/AOT 编译、内存分配、线程调度 执行流程

五、总结与个人体会

ART 与 Compose 的交互,其实是一个编译期 + 运行时的协同设计。编译器插件负责生成高效的字节码,Compose Runtime 负责在 ART 上管理重组,Slot 表则充当了状态与位置的桥梁。

我做了这么多年 Android 系统开发,最大的感受是:理解底层机制,能帮你写出更高效的代码。比如,知道 Slot 表的工作原理后,你就会主动使用 key()remember,而不是盲目堆代码。

嗯,今天就聊到这里。如果你在实际项目中遇到过 Compose 性能问题,不妨从 Slot 表和编译器插件入手分析——往往能发现意想不到的优化空间。

一句话总结:Compose 的流畅运行,离不开 ART 的运行时能力、Slot 表的高效重组、以及编译器插件的编译期优化。三者缺一不可。

公众号:蓝海资料掘金营,微信deep3321