Administrator
发布于 2026-08-25 / 28 阅读
0
0

系列文章《Android 工程师的 100 篇进阶笔记》第 68 篇:Compose 编译器原理:从 @Composable 到 UI 树

上一篇讲了 WebView 优化,回到 Compose——理解 Compose 的工作原理能让"调 API"的工程师变成"懂魔法"的工程师。Compose 的编译器插件是怎么把函数变成 UI 的?这篇拆开说。

Compose 编译器的"魔法"

Compose 不是普通的 Kotlin 函数调用。每个 @Composable 函数经过编译后会变化很大:

// 原始写法
@Composable
fun Greeting(name: String) {
    Text("Hello, $name")
}

// 编译后等价的伪代码
fun Greeting(name: String, $composer: Composer, $changed: Int) {
    $composer.startReplaceableGroup(0x1234ABCD)
    
    if ($changed and 1.inv() != 0 || $changed and 0b110 != 0) {
        $composer.startReplaceableGroup(0x5678EFGH)
        Text("Hello, $name", $composer, 0)
        $composer.endReplaceableGroup()
    }
    
    $composer.endReplaceableGroup()
}

编译器在 IR 阶段插入 Composer 的 start/end 调用、参数追踪。这就是 Compose Compiler Plugin 的核心工作。

编译器插件做了什么

Compose 编译器在编译期插桩:

1. 添加 Composer 参数

// before
fun MyView(text: String)

// after
fun MyView(text: String, $composer: Composer, $changed: Int)

每个 Composable 函数自动加上 Composer、changed mask 参数。

2. 追踪调用顺序

// before
Column {
    Text("Line 1")
    Text("Line 2")
}

// after(伪代码)
Column($composer, $changed) {
    Text("Line 1", $composer, 0)
    Text("Line 2", $composer, 0)
}

Composer 用 group index 追踪位置,下次重组时跳过相同位置。

3. 跳过未变化部分

changed 参数告诉 Composer "状态变了",决定是否重新执行函数。

@Composable
fun HeavyView() {
    Log.d("HeavyView", "执行了一次") // 每次重组都打印吗?
}

// 但实际上 Composer 配合 Skipping 会跳过未变更函数

Skip 机制:

条件

是否 Skip

参数全部相等

是

参数有 MutableState 引用

否

函数体用了 SideEffect

否

推断失败

否(保守 Skip=false)

Composer 的数据结构

Composer 是一个有状态的对象,内部结构基于 Slot Table(槽表):

Slot Table 简化视图:
       Group 0 (整棵树)
      /       \
  Group 1    Group 5
  /    \        |
Child  Child   ...

Group 有:
- key
- 父 group 索引
- 子节点列表
- 当前要执行的"游标"

每次 Compose 函数执行:

group 0 → 0x1234ABCD
  group 1 (Title) → 0x5678EFGH
    child: Text("Hello", $composer, 0)
  group 2 (Body) → 0x7890ABCD
    child: Text("World", $composer, 0)

Composer 用 position index 标识每个位置,下次重组时按位置对比。

重组的工作机制

Composable 函数在状态变化时不会"全部重渲染",只重渲染受影响部分:

@Composable
fun ProfileScreen(user: User) {
    Header(user.name)   // 重组只在 user.name 变化
    Posts(user.posts)   // 重组只在 user.posts 变化
}

// 如果只 user.name 变了:
// - ProfileScreen 重新执行
// - 但 Posts 函数内 Composer 看到参数没变 → Skip
// - 只 Header 内部重新执行

这就是 Compose 的"智能重组"。

性能陷阱:不稳定函数

Compose 编译器不能推断 "某些类型是不稳定的":

data class Item(
    val title: String,
    @Stable val count: Int
)

@Composable 函数签名里有 List<Item> 参数——List 不稳定,会导致不必要的重组。

@Stable 注解告诉编译器:"这个类的所有字段都是稳定的,相等性可比较":

@Stable
data class Item(
    val title: String,
    val count: Int
)

优化手段:

@Composable
fun MyList(items: List<Item>) {
    LazyColumn {
        items(items, key = { it.title }) { // key 帮助 recompose 跳过重复项
            Row {
                Text(it.title)
                Text("${it.count}")
            }
        }
    }
}

key 让 Compose 区分不同 item,跳过重新组合已存在的项。

stability 推断规则

类型

是否稳定

String

是

Int, Long, Double 等基本类型

是

data class 全字段稳定

是

List / MutableList

否(接口稳定性不能保证)

Map / MutableMap

否

@Immutable 注解类

是

@Stable 注解类

是(确保不会改 hash/equals)

实际上 Kotlin 的 data class 不等于 Compose 意义的 Stable。

Compiler Reports

Compose 编译器能生成报告,告诉你哪些函数"不稳定":

// build.gradle.kts
kotlinOptions {
    freeCompilerArgs += listOf(
        "-P",
        "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination="
            + project.buildDir.absolutePath + "/compose-reports"
    )
}

// generateComposition → true,输出所有 composable 函数的稳定性分析

报告目录:

build/compose-reports/
├── columns.kt.txt
├── composable_funs.kt.txt
├── stability_completeness.txt
└── release_compat.kt.txt

看 stability_completeness.txt:

unstable class User {
  stable val name: String
  unstable val posts: List<Post>
}

这种 class 加 @Immutable 或 @Stable 即可。

Compiler Metrics

freeCompilerArgs += listOf(
    "-P",
    "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination="
        + project.buildDir.absolutePath + "/compose-metrics"
)

// 生成指标

compose-metrics/ 里包含:

  • *-classes.jar 报告

  • *-composables.txt

  • *-restartable.txt

  • *-skippable.txt

composables.txt 里:

restartable skippable fun MyComposableFunction(...): Unit
  restartable fun Helper(...)

"skippable" 是优化黄金标志——能跳过的函数越多,重组越少。

Composition Local

CompositionLocal 是 Compose 的"环境"机制,编译期处理为 StaticComposable:

val LocalTheme = compositionLocalOf<Theme> { defaultTheme }

@Composable
fun MyView() {
    val theme = LocalTheme.current
    Text("Hello", color = theme.textColor)
}

实现原理:编译期把 LocalTheme.current 替换为类似 $LocalTheme.getCurrent($composer) 的调用。$composer 维护一张动态作用域栈:

[Scope 1: Activity]
  LocalTheme = LightTheme
[Scope 2: Dialog]
  LocalTheme = DarkTheme

重组时 Composer 检测 LocalTheme 值变化才重新执行。

Recomposition Counter

Compose 调试工具:

@Composable
fun ProfileScreen(name: String) {
    val count = remember { mutableIntStateOf(0) }
    
    SideEffect {
        // 每组合/重组后执行
        println("ProfileScreen recomposed")
    }
    
    Text("Hello, $name")
    Text("Recomposition count: ${count.intValue}")
    
    Button(onClick = { count.intValue++ }) {
        Text("Click")
    }
}

实测推荐用 Layout Inspector(Android Studio 内置):

Run > Open Layout Inspector
  → Recomposition counts

每个 Composable 显示重组次数:

ProfileScreen: 5 (Total)
  Header: 3
  Posts: 2

次数高的节点是优化目标。

性能实战经验

1. lambda 重组

// 反模式:每次重组都创建新 lambda
@Composable
fun MyList(items: List<Item>) {
    LazyColumn {
        items(items) { item ->
            Text(item.title) // lambda 也重组
        }
    }
}

// 改良:remember 包 lambda
@Composable
fun MyList(items: List<Item>) {
    val renderItem = remember { { item: Item ->
        Text(item.title)
    } }
    
    LazyColumn {
        items(items) { renderItem(it) }
    }
}

2. 不要在重组中分配大对象

// 反模式
@Composable
fun MyView(items: List<Item>) {
    val json = Json.encodeToString(items) // 序列化大对象
    
    Text(json)
}

// 改良:用 LaunchedEffect / SideEffect 隔离计算
val json = remember(items) {
    Json.encodeToString(items)
}

3. Compose 与 View 互操作

// Compose 包 View
AndroidView(factory = { context ->
    WebView(context).apply { /* 初始化 */ }
}, update = { /* 重组时更新 */ })

// View 包 Compose
ComposeView.setContent { MyComposable() }

互操作有性能开销(interop layer),关键页面尽量纯 Compose 或纯 View,不要频繁切。

2.0 编译器的新能力

Compose Compiler 2.0 引入了 K2 编译器支持:

freeCompilerArgs += listOf(
    "-P",
    "plugin:androidx.compose.compiler.plugins.kotlin:enableK2Composer=true"
)

K2 编译器生成 IR 更简洁,启动快 20%。

新强类型 API:

@Composable
fun rememberState(value: Int): State<Int> {
    return remember { mutableIntStateOf(value) }
}

// 2.0 后:derivedStateOf 性能更好
val displayPosts by remember(posts) {
    derivedStateOf { posts.filter { it.shouldShow } }
}

小结

Compose 编译器的核心是:插入 Composer 参数追踪调用、生成 group 索引、智能 Skip 重渲染。理解这一层后,写 Composable 时自然会注意:

  1. 让函数 Skip(参数稳定性)

  2. 用 key 标识列表项

  3. lambda 不要每次分配

  4. 重组中避免大计算

接下来三年 Compose 会逐步成为 Android UI 主流,编译器也在持续演进。掌握原理比掌握 API 更重要。

下一篇聊聊 Scoped Storage 与文件存储,理解 Android 10+ 的存储权限变迁。


评论