当面对会导致性能问题的不稳定的类时,您应使其保持稳定。本文档概要介绍了几种可用于实现此目的的方法。
启用强跳过
您应先尝试启用强效跳过模式。强跳过模式允许跳过参数不稳定的可组合项,是解决稳定性导致的性能问题的最简单方法。
如需了解详情,请参阅强跳过。
将类设为不可变
您还可以尝试使不稳定的类完全不可变。
- 不可变:表示一种类型,在该类型中,任何属性的值在构建该类型的实例后都不能更改,并且所有方法都优先透明。
- 请确保该类的所有属性均为
val
(而非var
),且均为不可变类型。 String, Int
和Float
等基元类型始终不可变。- 如果无法做到这一点,您必须对任何可变属性使用 Compose 状态。
- 请确保该类的所有属性均为
- 稳定:表示可变的类型。Compose 运行时不会知道该类型的任何公共属性或方法行为是否以及何时会产生与先前调用的不同结果。
不可变集合
Compose 认为某个类不稳定的一个常见原因是集合。如诊断稳定性问题页面中所述,Compose 编译器无法完全确定 List, Map
和 Set
等集合是否确实不可变,因此会将其标记为不稳定。
为了解决此问题,您可以使用不可变集合。Compose 编译器支持 Kotlinx 不可变集合。这些集合一定会是不可变的,Compose 编译器也会这样对待。此库仍处于 Alpha 版,因此其 API 可能会发生变化。
再次考虑一下诊断稳定性问题指南中的这个不稳定类:
unstable class Snack {
…
unstable val tags: Set<String>
…
}
您可以使用不可变集合使 tags
保持稳定。在类中,将 tags
的类型更改为 ImmutableSet<String>
:
data class Snack{
…
val tags: ImmutableSet<String> = persistentSetOf()
…
}
执行此操作后,该类的所有参数都是不可变的,并且 Compose 编译器会将该类标记为稳定。
使用 Stable
或 Immutable
添加注释
解决稳定性问题的一种可能方法是使用 @Stable
或 @Immutable
为不稳定的类添加注解。
为类添加注解会覆盖编译器本来可以推断的类。它类似于 Kotlin 中的 !!
运算符。您应非常谨慎地如何使用这些注解。替换编译器行为可能会导致意外的 bug,例如可组合项没有按预期重组。
如果在不使用注解的情况下能够使类保持稳定,您应努力以这种方式实现稳定性。
以下代码段提供了一个注释为不可变的数据类的极简示例:
@Immutable
data class Snack(
…
)
无论您使用 @Immutable
还是 @Stable
注解,Compose 编译器都会将 Snack
类标记为稳定。
集合中的类(带注解)
假设某个可组合项包含类型为 List<Snack>
的参数:
restartable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
…
unstable snacks: List<Snack>
…
)
即使您使用 @Immutable
为 Snack
添加了注解,Compose 编译器仍会将 HighlightedSnacks
中的 snacks
参数标记为不稳定。
当涉及到集合类型时,参数与类面临相同的问题,Compose 编译器始终将 List
类型的参数标记为不稳定,即使它是稳定类型的集合也是如此。
您无法将单个参数标记为稳定,也无法将可组合项注释为始终可跳过。前进道路有多种。
您可以通过多种方式解决集合不稳定的问题。以下各小节概述了这些不同的方法。
配置文件
如果您愿意遵守代码库中的稳定性协定,则可以选择将 Kotlin 集合视为稳定版,只需将 kotlin.collections.*
添加到您的稳定性配置文件中即可。
不可变集合
为确保不可变性的编译时安全性,您可以使用 kotlinx 不可变集合,而非 List
。
@Composable
private fun HighlightedSnacks(
…
snacks: ImmutableList<Snack>,
…
)
Wrapper
如果您无法使用不可变集合,则可以创建自己的集合。为此,请将 List
封装在带有注解的稳定版类中。通用封装容器可能是该用例的最佳选择,具体取决于您的要求。
@Immutable
data class SnackCollection(
val snacks: List<Snack>
)
然后,您可以在可组合项中将其用作形参的类型。
@Composable
private fun HighlightedSnacks(
index: Int,
snacks: SnackCollection,
onSnackClick: (Long) -> Unit,
modifier: Modifier = Modifier
)
解决方案
采用上述任一方法后,Compose 编译器现在会将 HighlightedSnacks
可组合项同时标记为 skippable
和 restartable
。
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 Compiler 1.5.5 开始,您可以在编译时提供一个配置文件,其中包含要视为稳定的类。这样,您就可以将不受您控制的类(例如 LocalDateTime
等标准库类)视为稳定。
配置文件是一个纯文本文件,其中每行包含一个类。支持注释、单个和双通配符。 配置示例如下所示:
// Consider LocalDateTime stable
java.time.LocalDateTime
// Consider kotlin collections stable
kotlin.collections.*
// Consider my datalayer and all submodules stable
com.datalayer.**
// Consider my generic type stable based off it's first type parameter only
com.example.GenericClass<*,_>
如需启用此功能,请将配置文件的路径传递给 Compose 编译器选项。
Groovy
kotlinOptions {
freeCompilerArgs += [
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" +
project.absolutePath + "/compose_compiler_config.conf"
]
}
Kotlin
kotlinOptions {
freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" +
"${project.absolutePath}/compose_compiler_config.conf"
)
}
由于 Compose 编译器在项目中的每个模块上分别运行,因此您可以根据需要为不同模块提供不同的配置。或者,在项目的根级别部署一项配置,并将该路径传递给每个模块。
多个模块
另一个常见问题涉及多模块架构。Compose 编译器只有在某个类引用的所有非基元类型均已明确标记为“稳定”或位于同样使用 Compose 编译器构建的模块中时,它才能推断相应类是否稳定。
如果您的数据层与界面层位于不同的模块中(我们推荐采用这种方法),那么您可能会遇到问题。
解决方案
要解决此问题,您可以采用以下方法之一:
- 将类添加到您的 Compiler 配置文件中。
- 对数据层模块启用 Compose 编译器,或者视情况使用
@Stable
或@Immutable
标记您的类。- 这涉及向数据层添加 Compose 依赖项。不过,它只是 Compose 运行时的依赖项,而不是
Compose-UI
的依赖项。
- 这涉及向数据层添加 Compose 依赖项。不过,它只是 Compose 运行时的依赖项,而不是
- 在界面模块中,将您的数据层类封装在界面专用的封装容器类中。
如果外部库不使用 Compose 编译器,也会出现同样的问题。
并非每个可组合项都应该是可跳过的
努力解决稳定性问题时,您不应尝试让每个可组合项都可跳过。尝试这样做可能会导致过早优化,从而引入比修复问题更多的问题。
在很多情况下,可跳过式广告没有任何实际好处,并且可能会导致难以维护代码。例如:
- 不经常重组或根本不重组的可组合项。
- 一个本身仅调用可跳过的可组合项的可组合项。
- 包含大量参数且成本高昂的可组合项的实现。在这种情况下,检查任何参数是否发生变化的成本可能会高于低成本重组的成本。
如果可组合项可跳过,则会增加少量开销,可能不值得。如果您确定可重启的开销超出其价值,甚至可以将可组合项注释为不可重启。