本文译自「Improving the Smoothness of Android Compose Screens」,原文链接proandroiddev.com/improving-t…,由@ruyouwei发布于2026年10月22日。
随着 Android 开发从传统的 View 系统转向 Jetpack Compose,更现代的 Compose 方法带来了截然不同的开发体验。两者在工具和性能跟踪工作流程方面也存在显著差异。除了帧率监控之外,View 系统通常依赖 ANR 报告来识别问题堆栈跟踪。然而,Compose 会根据数据变化更新 UI,因此 ANR 堆栈跟踪可能无法直接指出导致问题的代码。
影响 Compose 页面性能的因素
Compose 会将页面状态保存在内存中。当页面状态发生变化时,它会触发 UI 相应部分的重组。以下因素会影响渲染性能:
- 重组触发的频率
- 方法参数是否稳定
- 触发状态变更的 UI 层级结构
稳定性是 Compose 用来决定是否可以跳过重组的因素之一。在日常开发中,将不会改变的值声明为 val 可以避免不必要的状态更新和重组。开源 IDE 插件也可以直接在编辑器中显示可组合函数的参数是否稳定。
Compose 性能问题的最常见原因是频繁的状态变化触发了过多的重组。识别频繁变化的状态是找到根本原因的关键。
监控 Compose 触发的 UI 更新
一种方法是收集所有状态和参数,在可组合函数重组时比较它们的值,并将差异打印到 logcat。Rebugger 等工具已经提供了这种功能,其 IDE 插件可以添加监控代码。
GitHub - theapache64/rebugger:一个简单的 Compose Multiplatform 库,旨在将重组的原因打印到你的控制台/Logcat 窗口中。
通过自动注入监控代码来最大程度减少侵入
大多数情况下,日常开发过程中无需监控状态变化。只有当出现性能问题时才需要监控。在其他情况下,让插桩代码不进入源代码,有助于保持代码库整洁。
添加和移除监控代码并非唯一的维护负担。随着业务逻辑的变更,可组合函数可能会增加或减少状态变量和参数,所有这些都需要手动维护。这增加了额外的工作量。是否可以改为在编译时自动注入监控代码,将其从发布构建中排除,仅在启用某个功能开关时才插入?
在 Android 构建流水线中,可以使用与 ASM 相关的工具(asm.ow2.io/)将监控代码注入到编译后的字节码中。
compileDebugKotlin / compileDebugJavaWithJavac
│
│ finalizedBy
▼
┌───────────────────────────────────────────────────────────────────────┐
│ Pass 1 — generateVariableInfoDebug (GenerateVariableInfoTask) │
│ reads build/tmp/kotlin-classes/debug/**/*.class │
│ for each method annotated @ComposableMonitor: │
│ record every local var (name, descriptor, signature, slot, │
│ isMutableState, line of its last ASTORE) │
│ writes build/asm-analysis/<Class>_<method>.json │
└───────────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────┐
│ Pass 2 — transformDebugClassesWithAsm (ComposableStateMonitorLogger…) │
│ for each method annotated @ComposableMonitor: │
│ load its JSON, then inject bytecode that │
│ • builds an ArrayList<Pair<String, Any>> on entry │
│ • adds every parameter on entry │
│ • adds every State/MutableState local right after it's stored │
│ • calls CollectAndPrintValues(...) before every exit │
└───────────────────────────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────┐
│ Runtime — composemonitorhelper │
│ CollectAndPrintValues → unwrap MutableState, strip "$delegate" │
│ Inspector → remember previous values, diff, Log.d("Inspector", …) │
└───────────────────────────────────────────────────────────────────────┘
实现过程始于一个注解:只需将 @ComposableMonitor 添加到需要监控状态变化的可组合函数即可。字节码注入插件可随时通过配置启用或禁用。
该实现会在编译时执行两遍字节码处理。第一遍收集状态变量的位置;第二遍注入监控代码。之所以需要两遍,是因为 ASM 必须先完成对方法的遍历,才能获取完整的局部变量符号集和帧布局。注入的代码有两个主要职责:收集所有状态和方法参数,以及在可组合函数触发重组时比较新旧值,以确定是哪个状态或参数发生了变化。
主要挑战在于,可组合函数生成的 JVM 字节码在经过 Compose 编译器编译后,可能与其源代码存在显著差异。编译器会添加一些检查,例如针对 UI 更新的脏检查。因此,插桩必须在不破坏栈帧中变量索引的情况下,找到正确的插入点。
更好的方法:为 Compose Multiplatform 编译
尽管 ASM 使得在 Android 上自动注入监控代码成为可能,但这种方法仍不完善。该插桩机制依赖于为可组合函数生成的字节码中的固定模式。由于 Compose 编译器会随着版本更新而不断发展和优化,匹配这些固定模式可能无法兼容未来的变化。此外,该监控工具仅支持 Android,而 Compose Multiplatform 目前已被广泛采用,需要更广泛的平台支持。因此,我们需要一种更好的代码注入方法。
Kotlin Multiplatform (KMP) 的核心编译原则是“一次编写,到处运行”——代码在每个平台上都能原生运行。与 Flutter 使用 Skia/Impeller 引擎或 React Native 使用 JavaScript 桥接器在非原生平台上封装虚拟机不同,KMP 使用统一的编译器前端分析代码,然后使用不同的编译器后端将相同的 Kotlin 源代码编译成适用于各个平台的原生二进制文件或字节码。
这样就可以通过修改 Kotlin 前端生成的 IR 来注入代码。
Kotlin source ──► FIR (frontend) ──► IR
│
▼
┌──────────────────────────────────────┐
│ composemonitor IR extension │ inserts plain @Composable calls
│ (MonitorFunctionTransformer) │ before every exit
└──────────────────────────────────────┘
│ -Xcompiler-plugin-order
▼
┌──────────────────────────────────────┐
│ Compose compiler IR extension │ adds $composer/$changed, groups,
│ │ skipping, to user code AND ours
└──────────────────────────────────────┘
│
▼
JVM / Android bytecode, Kotlin/Native, wasm, js backends
实现 IR 编译器扩展的主要挑战在于,不同 Kotlin 版本之间的 IR 编辑插件 API 存在差异。早期版本中,由于 API 不稳定,这些 API 也曾发生过更改。鉴于 Compose Monitor 编译器插件需要支持使用不同 Kotlin 版本的项目,因此它已针对大多数 Kotlin 2.0 及更高版本进行了适配。
user applies io.github.hellsong.composemonitor (Java, KGP 2.0 API)
│ reads the project's Kotlin version, e.g. 2.2.10
▼
KotlinVersionSupport.lineFor("2.2.10") → k22
│ getPluginArtifact()
▼
KGP resolves composemonitor-compiler-k22
= shared/ + compat/{params, lookup-reference, registrar-legacy}
compiled against Kotlin 2.2.0
▼
runs inside kotlinc 2.2.10 and transforms the IR before Compose
我为此创建了一个新的Gradle 插件,该插件已发布在GitHub上。你可以在下方找到源代码和使用说明:
GitHub - hellsong/ComposeStateMonitor:初始提交
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
保护原创,请勿转载!