解决稳定性问题

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

启用强力跳过

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

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

使类不可变

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

  • 不可变:表示一种类型,其中任何属性的值在构造该类型的实例后都永远无法更改,并且所有方法都是引用透明的。
    • 确保该类的所有属性均为 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>
  
)

即使您使用 @Immutable 注释 Snack,Compose 编译器仍会将 HighlightedSnacks 中的 snacks 参数标记为不稳定。

在集合类型方面,参数面临与类相同的问题,Compose 编译器始终将 List 类型的参数标记为不稳定,即使它是稳定类型的集合也是如此。

您无法将单个形参标记为稳定,也无法注释可组合函数以使其始终可跳过。未来有多种发展方向。

您可以通过多种方式解决集合不稳定的问题。 以下小节概述了这些不同的方法。

配置文件

如果您愿意遵守代码库中的稳定性合约,则可以通过向稳定性配置文件添加 kotlin.collections.* 来选择接受将 Kotlin 集合视为稳定。

不可变集合

为了在编译时确保不可变性,您可以使用 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
)

在重组期间,如果 HighlightedSnacks 的任何输入都没有发生变化,Compose 现在可以跳过 HighlightedSnacks

稳定性配置文件

从 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. 在界面模块中,将数据层类封装在界面专用封装类中。

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

并非每个可组合项都应可跳过

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

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

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

如果可组合项是可跳过的,则会增加少量开销,这可能并不值得。在您确定可重启性带来的开销过高时,您甚至可以将可组合函数注释为不可重启