 @Composable)
一、概念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: ListString, 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 InspectorAndroidStudio 中使用 Layout Inspector 可以查看重组次数和跳过次数。6.2 编译报告 Compose Compiler Reports6.2.1 旧版写法Groovy在模块级 build.gradle 添加如下代码编译后报告路径module/build/compose_compiler/。metrics.csv可组合函数的重组次数、稳定性标记。report.html可视化的类型稳定性分析标记Stable/Unstableandroid { ... } 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 ) } } }