修复稳定性问题

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

启用强跳过

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

有关更多信息,请参阅强跳过

使类不可变

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

  • 不可变:表示一种类型,其任何属性的值在实例构建后永远不会改变,并且所有方法都具有引用透明性。
    • 确保类的所有属性都是 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.collections.* 添加到稳定性配置文件中,选择将 Kotlin 集合视为稳定。

不可变集合

为了在编译时确保不可变性安全,您可以使用 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 可组合项标记为既可跳过又可重启

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 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. 将类添加到您的编译器配置文件中。
  2. 在数据层模块上启用 Compose 编译器,或酌情使用 @Stable@Immutable 标记您的类。
    • 这涉及将 Compose 依赖项添加到数据层。但是,这只是 Compose 运行时的依赖项,而不是 Compose-UI 的依赖项。
  3. 在您的 UI 模块中,将数据层类包装在 UI 特定的包装类中。

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

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

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

在许多情况下,可跳过性并没有真正的优势,反而可能导致难以维护的代码。例如:

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

当一个可组合项可跳过时,它会增加一点开销,这可能不值得。在您认为可重启的开销不值得的情况下,您甚至可以注解您的可组合项为不可重启