☰
Compose 基础 - 可组合函数(重组、稳定性) @Composable
2026/10/6 12:07:02 网站建设 项目流程

一、概念

1.1 @Composable 注解

使用 @Composable 注解的函数(可组合函数)就是一个Composable(可组合项),可被用于组合成界面(只是个函数,不是任何View控件,底层用的是Canvas),函数内部只需要描述界面形状和数据依赖而不用关心界面的更新。
  • 函数名首字母需要大写:为了辨识度。
  • 只能由其它组合函数调用。
  • 返回值Unit:用来描述控件的组合而不是构造控件。
  • 默认内部控件的排列为堆叠。
  • 复用率高的建议写在顶层函数。
@Composable fun Show(str: String) { Text(text = "Hello ${str}!") }

1.2 编译时流程

@Composable注解并非传统的APT(注解处理器),而是基于Kotlin编译器插件(Kotlin Compiler Plugin)实现的,Compose编译器将 Kotlin 代码转换为可执行字节码时生成重组相关的元数据。

解析阶段识别带有@Composable注解的函数,标记为可组合函数。
分析阶段检查函数的合法性(例如不能返回非 Unit 类型、不能在非 Composable 函数中直接调用),并进行参数的稳定型(Stability)检查以确定可跳过性。
生成阶段修改可组合函数的字节码,添加隐藏参数Composer(组合器,管理重组上下文)和Int类型的slotId(槽位 ID,标记函数在组合树中的位置)。
生成SlotTable(槽位表):记录可组合函数的调用顺序、参数值、状态依赖等元数据,为重组时的差异对比提供依据。
处理挂起函数兼容若Composable函数包含挂起逻辑,会结合协程上下文和Composer实现挂起后的重组恢复。
// 编译后的伪代码(实际为字节码层面修改) fun Greeting(name: String, composer: Composer, slotId: Int): Unit { composer.start(slotId) // 标记函数开始,关联槽位 if (composer.changed(name)) { // 检查参数是否变化,决定是否重组 Text(text = "Hello $name", composer = composer, slotId = slotId + 1) } composer.end() // 标记函数结束 }

1.2.1 稳定性分析

Restartable

可重启

可组合函数默认被标记为 Restartable 使其能够独立响应读取的状态发生变化时重行执行(除非手动添加注解 @NonRestartableComposable 就只能在父容器重组时被连带再次执行),编译器会为其生成额外的代码用于记录其在组合树中的位置(定义了一个重组作用域),当状态发生变化时就可以精确定位到这里并触发重组。

Skippable

可跳过

可组合函数的所有参数都是稳定类型且函数体不包含读取不稳定状态操作时会被标记为 Skippable。当参数内容未发生变化时 Compose 会跳过该函数的执行(包括其所有子元素),这种优化能够显著减少不必要的计算和渲染工作,确保真正需要更新的组件才会被处理。

注意:自 Kotlin 2.0.2 起,强跳模式会默认开启,所有可组合函数默认就是可重启可跳过的。稳定类型采用内容比较(内容相同则跳过),不稳定类型采用引用比较(地址值相同则跳过)。

1.3 运行时行为

当检测到状态发生变化时,读取了该状态的可组合项便通过重组来更新显示,该读取代码所在的代码块就是重组作用域,即运行时通过 Restartable/Skippable 标记决定重组范围。

注意:由于 Column()、Row()、Box()等是内联函数,编译后不是一个函数,如果内部有读取状态的行为,实际是外层在读取和重组,会引起不必要的外层重组导致重组作用域扩大。

二、重组 Recomposition

底层原理详见文章:《编译器视角下的黑科技》

2.1 智能重组(跳过重组)

根据入参是否变化来决定是否重组,入参和上次相同的话(指结构相等,Kotlin中的==)就会跳过重组,否则随父组合项重组而重组。当状态发生改变时,以接收该状态的组合项为起点,根据其所调用的子组合项的参数是否发生变化,来判断是否重组,并以此向下递归,实现高效重组(局部刷新)。(使用数据类型具备稳定性来增强效果)

@Composable fun Demo( title: String, items: List<String>, onItemClick: (String) -> Unit ) = Column { //title值改变时Text会重组,items值改变时不会引发 Text(text = title) //items中某个元素改变时onItemClick()会重组,title值改变时不会引发 LazyColumn { items(items) {item -> onItemClick(item) } } }

2.2 重组可以舍弃(乐观操作)

当重组还未完成时候,由于状态再次变化引起新的重组,会取消之前的重组(舍弃界面树)执行新的重组,导致中途打断代码再重新执行,要避免重组影响。

2.3 可能执行非常频繁(不可预测性)

刷新界面的次数和时机是不确定的,重组的执行可能非常频繁,耗时操作会导致卡顿(使用Effect API解决),多次调用函数会使内部的局部变量被初始化。

2.4 非顺序执行

如果组合函数之间存在调用关系(父子关系),那么是顺序执行的,如果是并行排列(书写),Compose会识别哪些元素优先级更高从而优先绘制(如Box中前景元素优先级更高),因此要保证它们之间相互独立,不要读取和修改同一全局变量。

2.5 并发执行

Compose可以利用多核心通过并行执行组合函数来优化重组,意味着组合函数可能执行在线程池中,因此调用方和被调方可能在不同线程上,会出现多线程并发问题,修改组合函数中的局部变量会得到错误值。

//Box和Text可能会在不同线程执行,这样num显示可能是错的 @Composable fun Demo() { var num = 0 Row { Box { repeat(10) { num ++ } } Text("$num") } }

三、重组优化

从重组作用域(Restartable)、参数稳定性(Skippable)两个维度进行优化。

Compose 重组优化 - 状态读取(缩小重组作用域)

Compose 重组优化 - 参数稳定性(跳过不必要重组)

六、调试稳定性

6.1 Layout Inspector

AndroidStudio 中使用 Layout Inspector 可以查看重组次数和跳过次数。

6.2 编译报告 Compose Compiler Reports

6.2.1 旧版写法(Groovy)

在模块级 build.gradle 添加如下代码,编译后报告路径:module/build/compose_compiler/。

  • metrics.csv:可组合函数的重组次数、稳定性标记。
  • report.html:可视化的类型稳定性分析(标记@Stable/@Unstable)
android { ... } composeCompiler { reportsDestination = layout.buildDirectory.dir("compose_compiler") metricsDestination = layout.buildDirectory.dir("compose_compiler") }

6.2.2 新版写法(Kotlin)

<模块名称>-classes.txt — 关于此模块中类的稳定性报告。

<模块名称>-composables.txt — 关于此模块中可组合项的可重启性和可跳过性的报告。

<模块名称>-composables.csv — 上述文本文件的CSV版本,用于导入电子表格或通过脚本进行处理。

android { kotlinOptions { if (project.findProperty("composeCompilerReports") == "true") if (project.findProperty("composeCompilerReports") == "true") { freeCompilerArgs += arrayOf( "-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + project.buildDir.absolutePath + "/compose_compiler" ) } if (project.findProperty("composeCompilerMetrics") == "true") { freeCompilerArgs += arrayOf( "-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + project.buildDir.absolutePath + "/compose_compiler" ) } } }

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询