📅 2026-05-25 👤 王强 🏷️ Kotlin 1.9 · Compose · 编译器

Kotlin 1.9 Compose 编译器:Composable 函数的 IR 变换深度

从 Compose 编译器的分组策略、remember/lazyListOf 的优化路径出发,解析 Skia 渲染管线、重组范围推断,以及 Kotlin 1.9 编译速度提升的幕后实现。

1. IR 变换与编译流水线

1.1 Compose 编译器的位置

Compose 不只是一个 UI 框架,它的核心是一套编译器插件(Compose Compiler Gradle Plugin)。这个插件在 Kotlin 编译流水线的 IR(Intermediate Representation)阶段插入,对 @Composable 注解的函数进行变换。

Kotlin Source
Kotlin IR
Compose Compiler
Skia DrawOps
Bitmap/Pixel
Compose 编译流水线

1.2 Composable 函数的 IR 表示

编译器在 IR 层识别所有带 @Composable 的函数,然后注入额外的控制流——在函数开始时检查当前是否处于"Composable 上下文",以及在每个可组合调用点插入 Composer 参数。

Composable 函数编译前后对比
// 源代码
@Composable
fun Greeting(name: String) {
    Text("Hello, $name")
}

// 编译后等价签名(伪代码)
fun Greeting(
    name: String,
    $composer: Composer,
    $changed: Int,
    $block: Int
) {
    if ($changed and 0x1 != 0) {
        Text(
            "Hello, $name",
            $composer,
            0,  // $changed
            0   // $block
        )
    }
}

1.3 Composer 参数的语义

$composer 参数是 Compose 记忆化(memorization)的核心。编译器通过 $changed 位掩码判断是否需要重组:

$changed 位掩码语义
// $changed 是一个 Int,包含多个维度的变化信息
// bit 0: this (this reference 是否变化)
// bit 1-2: position (位置是否变化)
// bit 3: data0 (第一个参数是否变化)
// bit 4: data1 (第二个参数是否变化)
// ...以此类推

// Composer 依据 $changed 判断是否复用缓存
// $changed & 0x1 != 0 → 需要重新执行
// 否则 → 复用上次渲染结果(记忆化)

2. 重组范围(Recompose Scope)推断

2.1 什么是重组范围

Compose 的重组(Recomposition)粒度精细到单个 @Composable 函数调用。编译器在 IR 阶段自动推断每个调用点的"重组范围",使得只有真正依赖变化数据的部分才重新执行。

重组范围推断示例
// 源代码:只有 data 变化时仅重组 Text(data)
@Composable
fun MyScreen(data: State) {
    Text("Header")           // 静态,不需要重组
    Text(data.value)           // 依赖 data,重组范围 #2
    Button("Click", ::onClick) // 静态,理论上不需要重组
}

// 编译后自动推断的重组范围
// 每个调用点被标记了 $scopeId
Text("Header", composer, 0, 0)           // scope: 0 (永远不变)
Text(data.value, composer, 0, 1)           // scope: 1 (依赖 data)
Button("Click", composer, 0, 2) // scope: 2 (静态但含 lambda)

2.2 remember 的优化机制

remember 是一个 Composable 函数,但其内部逻辑受到编译器的特殊处理——它会在编译时将计算结果缓存到 Composer 对象中:

remember 编译变换
// 源代码
@Composable
fun ExpensiveComponent(input: String) {
    val cached = remember(input) {
        expensiveCalculation(input)
    }
    Text(cached)
}

// 编译后(等价伪代码)
@Composable
fun ExpensiveComponent(input: String, $composer: Composer) {
    val cached = remember(input) {
        // 如果 input 没变,直接从 composer.cache 读取
        // 如果 input 变了,才执行 lambda
    }
    Text(cached, $composer, 0, 0)
}
💡 关键洞察

remember 的"跨重组保持"能力来源于 Composer 的内部缓存机制。编译器将 remember 的 key 转换为 Composer 内部的 SlotTable 索引,实现 O(1) 的缓存查找。

3. Skia 渲染管线

3.1 Compose 如何对接 Skia

Android Compose 使用 skia org.jetbrains.skia 作为渲染引擎。UI 指令被编译成 Skia 的 DrawOps(绘制操作),然后在 GPU 或 CPU 上执行:

Compose UI tree
LayoutNode → DrawOps
Skia Canvas
Hardware Buffer
Skia 渲染管线

3.2 LazyList 的优化路径

LazyColumn/LazyRow 是 Compose 最复杂的组件之一。其优化策略包括:

  • 惰性布局(Lazy Layout):只测量/绘制屏幕可见区域内的 item
  • 内容类型分组(ContentType):同类型 item 复用相同的测量缓存
  • 锚点定位(Anchor):滚动位置变化时快速定位到最近的 item
LazyColumn 编译优化示意
// 源代码
LazyColumn {
    items(items) { item ->
        ListItem(item)  // 每一项独立重组
    }
}

// 编译后 IR(分组策略)
// items() 调用被拆分为多个独立的 recompose scope
// 每个 ListItem 属于不同的 $scopeId
// 滚动时只重组新增/移出屏幕的 items

4. Kotlin 1.9 编译优化

4.1 编译速度提升的幕后

Kotlin 1.9 对 Compose 编译器做了重大优化,编译速度提升约 30-50%(官方数据),主要来自以下改进:

优化项 Kotlin 1.8 Kotlin 1.9 提升
IR 分组策略 逐函数遍历 批量并行处理 ~25%
remember 缓存查找 O(n) SlotTable 扫描 O(1) HashMap 查找 ~15%
Composer 内联 间接调用 try inline 内联 ~10%

4.2 分组策略(Group Merging)

Kotlin 1.9 引入了分组合并策略——对于静态内容,编译器会将多个独立的 Composable 调用合并为一个 DrawOps 组,减少绘制调用次数:

分组合并示例
// 源代码:三个静态 Text → 理想情况下合并为一个 DrawOp
Column {
    Text("Line 1")
    Text("Line 2")
    Text("Line 3")
}

// 编译后:静态 Text 被合并为单个 StaticText 绘制指令
// Skia 执行一次 drawTextRun() 代替三次 drawText()

5. 1.9 新特性与展望

5.1 Compose Compiler 1.5.0 核心变更

Kotlin 1.9 捆绑了 Compose Compiler 1.5.x,带来了以下新特性:

Kotlin 1.9 Compose 新特性
// 1. @Composable 函数的 return 语句现在可以正常工作
// (之前 return 会跳过重组范围)
@Composable
fun ConditionalContent(flag: Boolean) {
    if (flag) {
        return  // ✅ 现在允许
    }
    Text("Content")
}

// 2. Composer.skip() 语义更明确
// 编译器可以更好地推断哪些调用可以跳过

// 3. 改良的 remember { derivedStateOf() } 优化
// derivedStateOf() 结果会被更早地缓存

5.2 最佳实践建议

💡 架构建议
  • 避免在 Composable 函数中执行昂贵计算——用 remember 包装
  • 保持 @Composable 函数纯净(无副作用),以便编译器优化
  • 对于列表数据,优先使用 items(contentType = ...) 指定类型
  • 避免在 Composable 中创建新对象(触发重组)
性能友好的 Composable 写法
// ❌ 不推荐:每次重组创建新对象
@Composable
fun BadExample(items: List<String>) {
    LazyColumn {
        items(items.map { Item(it) }) { item ->  // map 每次创建新 List
            ListItem(item)
        }
    }
}

// ✅ 推荐:items 作为状态或 remember 化
@Composable
fun GoodExample(items: List<String>) {
    val itemObjects = remember(items) {
        items.map { Item(it) }
    }
    LazyColumn {
        items(itemObjects) { item ->
            ListItem(item)
        }
    }
}