修复稳定性问题

当遇到 不稳定的类 导致性能问题时,您应该使其稳定。本文档概述了您可以用来做到这一点的几种技术。

启用强跳过

您应该首先尝试启用强跳过模式。强跳过模式允许具有不稳定参数的可组合项被跳过,是修复由稳定性引起的性能问题的最简单方法。

有关详细信息,请参阅 强跳过

使类不可变

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

  • 不可变: 指示一种类型,其中任何属性的值在该类型的实例构造后永远不会改变,并且所有方法都是引用透明的。
    • 确保类的所有属性都是 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 中的操作符。您应该非常小心地使用这些注释。覆盖编译器行为可能会导致您遇到意想不到的错误,例如您的可组合项在您期望它重新组合时不会重新组合。

如果可以在没有注释的情况下使您的类稳定,您应该努力以这种方式实现稳定性。

以下代码片段提供了一个注释为不可变的数据类的最小示例

@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>,
    …
)

包装器

如果您不能使用不可变集合,您可以自己创建。为此,请将 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
)

在重新组合期间,如果 HighlightedSnacks 的任何输入都没有改变,Compose 现在可以跳过它。

稳定性配置文件

从 Compose 编译器 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 编译器构建的模块中时推断类是否稳定。

如果您的数据层与您的 UI 层在不同的模块中(这是推荐的方法),那么您可能会遇到此问题。

解决方案

要解决此问题,您可以采取以下方法之一

  1. 将这些类添加到您的 编译器配置文件 中。
  2. 在您的数据层模块上启用 Compose 编译器,或者在适当的地方使用 @Stable@Immutable 标记您的类。
    • 这涉及将 Compose 依赖项添加到您的数据层。但是,这只是 Compose 运行时的依赖项,而不是 Compose-UI 的依赖项。
  3. 在您的 UI 模块中,将您的数据层类包装在特定于 UI 的包装类中。

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

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

在解决稳定性问题时,您不应该试图使每个可组合项都可跳过。尝试这样做会导致过早优化,从而引入更多问题,而不是解决问题。

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

  • 一个不经常重新组合或根本不重新组合的可组合项。
  • 一个本身只调用可跳过可组合项的可组合项。
  • 一个具有大量参数且参数具有昂贵的 equals 实现的可组合项。在这种情况下,检查任何参数是否更改的成本可能超过便宜重新组合的成本。

当可组合项可跳过时,它会增加一小部分开销,这可能不值得。您甚至可以注释您的可组合项以使其 不可重新启动,在您确定重新启动的开销过高的情况下。