Compose编译器报告
Compose编译器可以输出其稳定性推断的结果以供检查。结合检查报告,你可以确定哪些可组合项是可跳过的,哪些不是。这篇文章总结了如何使用这些报告,但有关这些报告的详细信息,请参阅 技术文档。
⚠️ 警告:只有当你确实遇到与稳定性相关的性能问题时,才应使用此技术。试图让你的整个 UI 都可以跳过是过早优化,可能会导致未来的维护困难。在针对稳定性进行优化之前,请确保你遵循我们关于 Compose 性能的 最佳实践。
默认情况下不启用编译器报告。通过使用compiler flag来开启Compose编译器报告,具体设置因项目而异,但对于大多数项目,你可以将以下脚本粘贴到根 build.gradle 文件中。
/* Copyright 2022 Google LLC. SPDX-License-Identifier: Apache-2.0 */ subprojects { tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { kotlinOptions { if (project.findProperty("composeCompilerReports") == "true") { freeCompilerArgs += [ "-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + project.buildDir.absolutePath + "/compose_compiler" ] } if (project.findProperty("composeCompilerMetrics") == "true") { freeCompilerArgs += [ "-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + project.buildDir.absolutePath + "/compose_compiler" ] } } } }
要调试可组合项的稳定性,你可以运行以下task:
./gradlew assembleRelease -PcomposeCompilerReports=true
⚠️ 警告:确保始终在发布版本(release build )上运行它以确保准确的结果。
此任务将输出三个文件。 (包括来自Jetsnack的示例输出)
-classes.txt — 关于此模块中类的稳定性的报告。 案例。
-composables.txt — 关于此模块中可组合项的可重启性和可跳过性的报告。案例.
-composables.csv — 上述文本文件的 csv 版本,用于导入电子表格或通过脚本处理。 案例.
如果你改为运行 composeCompilerMetrics
任务,你将获得项目中可组合项数量的总体统计信息和其他类似信息。这在这篇文章中没有涉及,因为它对调试没有那么有用。 打开 composables.txt
文件,你将看到该模块的所有可组合函数,并且每个函数都将标记它们是否可重新启动、可跳过及其参数的稳定性。这是来自Jetsnack 的一个假设示例,它是 Compose 示例应用程序之一。
restartable skippable scheme(“[androidx.compose.ui.UiComposable]”) fun SnackCollection( stable snackCollection: SnackCollection stable onSnackClick: Function1<Long, Unit> stable modifier: Modifier? = @static Companion stable index: Int = @static 0 stable highlight: Boolean = @static true )
此 SnackCollection
可组合项完全可重启、可跳过且稳定。在可能的情况下,这通常是你想要的,尽管远非强制性的(博文末尾有更多详细信息)。
但是,让我们看另一个例子。
restartable scheme(“[androidx.compose.ui.UiComposable]”) fun HighlightedSnacks( stable index: Int unstable snacks: List<Snack> stable onSnackClick: Function1<Long, Unit> stable modifier: Modifier? = @static Companion )
HighlightedSnacks
可组合项是不可跳过的——只要在重组期间调用它,即使它的参数都没有改变,它也会重组。 这是由不稳定的参数snacks
引起的。
现在我们来到 classes.txt 文件来检查 Snack
的稳定性。
unstable class Snack { stable val id: Long stable val name: String stable val imageUrl: String stable val price: Long stable val tagline: String unstable val tags: Set<String> <runtime stability> = Unstable }
作为参考,这是 Snack
的声明方式
data class Snack( val id: Long, val name: String, val imageUrl: String, val price: Long, val tagline: String = "", val tags: Set<String> = emptySet() )
snacks
是不稳定的。它的绝大部分参数都是稳定的,但tags
被认为是不稳定的。但这是为什么呢? Set
看起来是不可变的,它不是 MutableSet
。 不幸的是,Set(以及 List
和其他标准集合类,稍后会详细介绍)在 Kotlin 中被定义为接口,这意味着底层实现可能仍然是可变的。例如,你可以写:
val set: Set<String> = mutableSetOf(“foo”)
变量是常量,它声明的类型不是可变的,但它的实现仍然是可变的。 Compose 编译器无法确定此类的不变性,因为它只看到声明的类型,因此将其声明为不稳定的。现在让我们看看如何使它稳定。
让不稳定稳定(Stabilizing the unstable)
当不稳定类导致了性能问题时,尝试使其稳定是个好主意。首先要尝试的是让类完全不可变。
不可变——表示一种类型,其中任何属性的值在构造对象后都不会改变,并且所有方法都是引用透明的。所有基本类型(
String
、Int
、Float
等)都被认为是不可变的。
换句话说,将所有 var 属性设为 val,并将所有这些属性设为不可变类型。
如果你无法实现上述要求,那你将不得不对任何可变属性使用 Compose State。
稳定——表示一种类型是可变的,但如果任何公共属性或方法行为会产生与先前调用不同的结果,Compose 运行时将收到通知(译者:虽然对象内部的数值虽然会发生变化,但是这种变化可以被Compose识别)。
这意味着在实践中,任何可变属性都应该由 Compose 状态支持,例如 mutableStateOf(…)
。
回到 Snack
示例,该类看起来是不可变的,那么我们如何解决它呢?
你可以采取以下方法:
Kotlinx 不可变集合(Immutable Collections)
Compose 编译器的 1.2 版包括对 Kotlinx Immutable Collections的支持。这些集合保证是不可变的,并且将由编译器推断为不可变的。该库仍处于 alpha 阶段,因此预计其 API 可能会发生变化。你应该评估这对你的项目是否可以接受。
将tags
的类型改变为下面这种类型可以让Snack
稳定
val tags: ImmutableSet<String> = persistentSetOf()
使用Stable或者Immutable注释
根据上述规则,类也可以使用 @Stable 或 @Immutable 进行注释。
⚠️ 警告:非常需要注意的是,这是一个约定,要遵循相应的注解规则。它本身不会使类不可变/稳定。错误地使用注释可能会导致重组失败。
注释一个类会覆盖编译器对你的类的推断,这样它类似于kotlin的!!运算符。你应该非常小心这些注释的使用,因为如果你弄错了,覆盖编译器行为可能会导致你出现无法预料的错误。如果可以在没有注释的情况下使您的类稳定,那么你应该努力以这种方式实现稳定。
正确注释Snack
的方式如下:
/* Copyright 2022 Google LLC. SPDX-License-Identifier: Apache-2.0 */ @Immutable data class Snack( val id: Long, val name: String, val imageUrl: String, val price: Long, val tagline: String = "", val tags: Set<String> = emptySet() )
无论选择哪种方法,Snack
类都将被推断为稳定的。
但是,回到 HighlightedSnacks
可组合项,HighlightedSnacks
仍未标记为可跳过:
unstable snacks: List<Snack>
当涉及到集合类型时,参数面临与类相同的问题,List
总是被确定为不稳定的,即使它是稳定类型的集合。 你也不能将单个参数标记为稳定,也不能将可组合项注释为始终可跳过。所以,你可以做什么?同样,也有很多种方法解决这个问题。
使用Kotlinx 不可变集合而不是List
:
/* Copyright 2022 Google LLC. SPDX-License-Identifier: Apache-2.0 */ @Composable private fun HighlightedSnacks( index: Int, snacks: ImmutableList<Snack>, onSnackClick: (Long) -> Unit, modifier: Modifier = Modifier )
如果你不能使用不可变集合,你可以在最简单的情况下将List
包装在带注释的稳定类中,以将其标记为对 Compose 编译器不可变。
@Immutable data class SnackCollection( val snacks: List<Snack> )
然后,你可以将其用作可组合项中的参数类型。
@Composable private fun HighlightedSnacks( index: Int, snacks: SnackCollection, onSnackClick: (Long) -> Unit, modifier: Modifier = Modifier )
在采用其中任何一种方法后,HighlightedSnacks
可组合项现在既可以跳过也可以重新启动。
restartable skippable scheme(“[androidx.compose.ui.UiComposable]”) fun HighlightedSnacks( stable index: Int stable snacks: ImmutableList<Snack> stable onSnackClick: Function1<Long, Unit> stable modifier: Modifier? = @static Companion )
HighlightedSnacks
现在将在其入参均未更改时跳过重组。
多模块
你可能遇到的另一个常见问题与多模块架构有关。 Compose 编译器推断一个类是否稳定,前提是它引用的所有非原始类型都被显式标记为稳定的,而且位于Compose 编译器构建的模块中。如果你的数据层(data layer)和UI层(UI layer,)是分开的(这是推荐的方法),这可能是你会遇到的问题。要解决此问题,你可以:
- 在你的数据层模块上启用 Compose 编译器,或在适当的地方使用 @Stable 或 @Immutable 标记你的类。
- 这将涉及向数据层添加 Compose 依赖项,你只需要添加Compose运行时的依赖而不用添加Compose-UI依赖。
- 将你的数据层的类包装在你的UI层的特定包装类中。
同样的问题也会发生在外部module上,除非它们使用的是 Compose 编译器。
这是一个已知的限制,我们目前正在研究针对多模块架构和外部库的更好解决方案。
所有的重组都应该被跳过吗?
不。
追求应用中每个可组合项的完全可跳过性是不成熟的优化。可跳过实际上会增加其自身的少量开销,这可能不值得,如果你确定可重启的开销大于其价值,你甚至可以将可组合项注释为 不可重启。在许多其他情况下,可跳过不会有任何实际好处,只会导致难以维护代码。例如:
- 不经常重组或根本不重组的可组合项。
- 只是被称为可跳过但是实际上项目中没有跳过的场景的可组合项。
总结
这篇博文中有很多信息,所以让我们总结一下。
- Compose 查看可组合项的每个参数的稳定性,以确定在重组期间是否可以跳过它。
- 如果你注意到你的可组合项没有被跳过并且它导致了性能问题,你应该首先检查不稳定的明显原因,例如 var 参数。
- 你可以使用编译器报告来确定所推断的关于你的类的稳定性。
- 像
List
、Set
和 Map 这样的集合类总是被确定为不稳定的,因为不能保证它们是不可变的。你可以改用 Kotlinx 不可变集合,或将你的类注释为@Immutable
或@Stable
- 来自未运行 Compose 编译器的module的类始终被确定为不稳定。添加 compose 运行时的依赖,并在你的模块中将它们标记为稳定,或根据需要将类包装在 UI model类中
- 每个可组合项都应该是可跳过的吗?不。
有关 Compose 性能的更多调试技巧,请查看我们的最佳实践指南 和 I/O talk。