解决稳定性问题

如果遇到导致性能问题的不稳定类,您应使其稳定。本文档简要介绍了可用于实现此目的的几种方法。

启用强劲跳过

您应先尝试启用强力跳过模式。强跳过模式允许跳过参数不稳定的可组合项,并且是修复由稳定性导致的性能问题的最简单方法。

如需了解详情,请参阅强制跳过

将类设为不可变

您还可以尝试使不稳定的类完全不可变。

  • 不可变:表示一种类型,在构造某种类型的实例后,其任何属性的值都永远不会更改,并且所有方法实际上都是透明的。
    • 确保该类的所有属性均为 val(而非 var),并且是不可变类型。
    • String, IntFloat 等基元类型始终是不可变的。
    • 如果无法做到这一点,则必须对所有可变属性使用 Compose 状态。
  • 稳定:表示可变的类型。Compose 运行时无法感知该类型的任何公共属性或方法行为是否以及何时会产生与先前调用不同的结果。

不可变集合

Compose 认为类不稳定的常见原因是集合。如诊断稳定性问题页面中所述,Compose 编译器无法完全确定 List, MapSet 等集合是否确实不可变,因此会将它们标记为不稳定。

如需解决此问题,您可以使用不可变集合。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 编译器会将该类标记为稳定。

使用 StableImmutable 进行注解

解决稳定性问题的一种可能方法是使用 @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>
  …
)

即使您为 Snack 添加了 @Immutable 注解,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 可组合项同时标记为 skippablerestartable

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
)

在重组期间,如果 Compose 的所有输入均未更改,现在可以跳过 HighlightedSnacks

稳定性配置文件

从 Compose Compiler 1.5.5 开始,您可以在编译时提供要视为稳定的类的配置文件。这样您就可以将不由您控制的类(例如 LocalDateTime 之类的标准库类)视为稳定。

配置文件是一个纯文本文件,每行包含一个类。支持使用注释、单通配符和双通配符。配置示例:

// Consider LocalDateTime stable
java.time.LocalDateTime
// Consider my datalayer stable
com.datalayer.*
// 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 编译器 Gradle 插件配置的 composeCompiler 选项块。

composeCompiler {
  stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_config.conf")
}

由于 Compose 编译器会在项目中的每个模块上分别运行,因此您可以根据需要为不同模块提供不同的配置。或者,在项目的根级别有一个配置,并将该路径传递给每个模块。

多个模块

另一个常见问题涉及多模块架构。只有当类引用的所有非基元类型都明确标记为稳定或位于也使用 Compose 编译器构建的模块中时,Compose 编译器才能推断类是否稳定。

如果您的数据层位于与界面层分开的模块中(这是推荐的方法),您可能会遇到此问题。

解决方案

如需解决此问题,您可以采用以下任一方法:

  1. 将类添加到 Compiler 配置文件中。
  2. 在数据层模块上启用 Compose 编译器,或者在适当的情况下使用 @Stable@Immutable 标记类。
    • 这涉及向数据层添加 Compose 依赖项。不过,它只是 Compose 运行时的依赖项,而不是 Compose-UI 的依赖项。
  3. 在界面模块中,将数据层类封装在界面专用封装容器类中。

如果外部库不使用 Compose 编译器,也会出现同样的问题。

并非所有可组合项都应可跳过

在解决稳定性问题时,请勿尝试让所有可组合项都可跳过。尝试这样做可能会导致过早优化,从而导致问题多于修复。

在许多情况下,可跳过的广告没有任何实际好处,并且可能会导致代码难以维护。例如:

  • 不经常重组或根本不重组的可组合项。
  • 本身只调用可跳过的可组合项的可组合项。
  • 具有大量参数以及开销非常大的 equals 实现的可组合项。在这种情况下,检查任何参数是否发生更改的成本可能会超过低成本重组的成本。

当可组合项可跳过时,它会增加一小部分开销,这可能不值得。如果您确定可重启的开销超出了其价值,您甚至可以将可组合项注解为“不可重启”。