提升 Android Compose 页面的流畅度

本文译自「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 相应部分的重组。以下因素会影响渲染性能:

  1. 重组触发的频率
  2. 方法参数是否稳定
  3. 触发状态变更的 UI 层级结构

稳定性是 Compose 用来决定是否可以跳过重组的因素之一。在日常开发中,将不会改变的值声明为 val 可以避免不必要的状态更新和重组。开源 IDE 插件也可以直接在编辑器中显示可组合函数的参数是否稳定。

Compose Stability Analyzer

Compose 性能问题的最常见原因是频繁的状态变化触发了过多的重组。识别频繁变化的状态是找到根本原因的关键。

监控 Compose 触发的 UI 更新

一种方法是收集所有状态和参数,在可组合函数重组时比较它们的值,并将差异打印到 logcat。Rebugger 等工具已经提供了这种功能,其 IDE 插件可以添加监控代码。

GitHub - theapache64/rebugger:一个简单的 Compose Multiplatform 库,旨在将重组的原因打印到你的控制台/Logcat 窗口中。

通过自动注入监控代码来最大程度减少侵入

大多数情况下,日常开发过程中无需监控状态变化。只有当出现性能问题时才需要监控。在其他情况下,让插桩代码不进入源代码,有助于保持代码库整洁。

添加和移除监控代码并非唯一的维护负担。随着业务逻辑的变更,可组合函数可能会增加或减少状态变量和参数,所有这些都需要手动维护。这增加了额外的工作量。是否可以改为在编译时自动注入监控代码,将其从发布构建中排除,仅在启用某个功能开关时才插入?

在 Android 构建流水线中,可以使用与 ASM 相关的工具(asm.ow2.io/)将监控代码注入到编译后的字节码中。

sql 复制代码
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 来注入代码。

bash 复制代码
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 及更高版本进行了适配。

scss 复制代码
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:初始提交

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!

相关推荐
撩得Android一次心动3 小时前
Android中Gradle网络下载超时、更改存放位置(解决方案)
android·gradle
HQL_seven5 小时前
【Activity】Android Activity详解
android
AI2中文网5 小时前
安卓 15/16 手机软键盘遮挡输入框、布局顶不上去?已解决:重新编译即可
android·智能手机
字节暗面5 小时前
Android APP 加固横评记录:爱加密、梆梆、网易易盾与腾讯 ACE
android·逆向工程·加固
落魄Android在线炒饭5 小时前
Android 实战问题反推 OpenGL 概念(上)
android·opengl
booboosui5 小时前
DeepSeek 的 Worker 调得动 GitHub,调不动微信:三类 Worker 的实现差别
android
MiyamuraMiyako7 小时前
Compose LazyRow 实现尾项露出与少量数据均分
android·android jetpack
booboosui7 小时前
输入法借 350 毫秒:给 agent 打字的两条路
android
灿飞7 小时前
Kotlin与Java虚拟机四:一段代码的完整旅程
kotlin