Compose 中的稳定性

Compose 会将类型视为稳定或不稳定。如果类型不可变,或者 Compose 可以知道其值在重组之间是否发生了变化,则该类型就是稳定的类型。如果 Compose 无法知道类型的值在重组之间是否发生了变化,则该类型就是不稳定的类型。

Compose 会使用可组合项参数的稳定性来确定如何比较输入,并决定是否可以在重组期间跳过可组合项(从 Kotlin 2.0.20 开始,默认启用强跳过模式):

  • 稳定参数:Compose 使用结构相等性 (Object.equals()) 比较稳定参数。如果可组合函数的稳定参数等于其之前的值,Compose 会跳过该函数。
  • 不稳定的参数:在启用强力跳过模式(Kotlin 2.0.20 及更高版本中的默认设置)的情况下,Compose 会使用实例相等性 (===) 比较不稳定的参数,如果传递的是相同的对象实例,则会跳过可组合函数。在 Kotlin 2.0.20 之前的版本中(或当强力跳过模式处于停用状态时),当父可组合函数重组时,Compose 始终会重组具有不稳定形参的可组合函数。

如果您的应用经常分配不稳定的形参的新实例,或者通过过度注释模型来强制对大型集合执行开销高昂的 .equals() 检查,您可能会发现不必要的重组或比较开销。

本文档详细介绍了 Compose 如何确定稳定性,以及如何对其进行优化以提升性能和整体用户体验。

不可变对象

以下代码段演示了稳定性和重组背后的一般原则。

Contact 类是一个不可变的数据类。这是因为其所有形参都是使用 val 关键字定义的原基元。创建 Contact 的实例后,您无法更改对象属性的值。如果您尝试这样做,则会创建一个新对象。

data class Contact(val name: String, val number: String)

ContactRow 可组合项具有类型为 Contact 的参数。

@Composable
fun ContactRow(contact: Contact, modifier: Modifier = Modifier) {
   var selected by remember { mutableStateOf(false) }

   Row(modifier) {
      ContactDetails(contact)
      ToggleButton(selected, onToggled = { selected = !selected })
   }
}

不妨考虑一下,当用户点击切换按钮且 selected 状态发生变化时会发生什么:

  1. Compose 会评估是否应重新组合 ContactRow 内的代码。
  2. 它发现 ContactDetails 的唯一实参是 Contact 类型。
  3. 由于 Contact 是不可变的数据类,因此 Compose 可以确定 ContactDetails 的所有实参均未发生变化。
  4. 因此,Compose 会跳过 ContactDetails,不会重组它。
  5. 另一方面,ToggleButton 的实参已发生变化,因此 Compose 会重组该组件。

可变对象

虽然前面的示例使用了不可变对象,但也可以创建可变对象。请看以下代码段:

data class Contact(var name: String, var number: String)

由于 Contact 的每个形参现在都是 var,因此该类不再是不可变的。 如果其属性发生更改,Compose 也不会感知到。这是因为 Compose 只跟踪对 Compose 状态对象的更改。

Compose 会将此类视为不稳定。在强跳过模式 (Kotlin 2.0.20+) 下,Compose 会使用实例相等性 (===) 比较 Contact:就地变异 contact.name 不会触发重组,而传递具有相同值的新分配的 Contact 实例仍会强制 ContactDetails 重组(如果没有强跳过模式,ContactDetails 会在每次 selected 更改时重组)。

Compose 中的实现

考虑 Compose 在重组期间如何准确确定要跳过哪些函数,虽然不是至关重要,但很有帮助。

当 Compose 编译器在您的代码上运行时,它会使用多个标记之一来标记每个函数和类型。这些标记反映了 Compose 在重组期间如何处理函数或类型。

函数

Compose 可以将函数标记为 skippable 或 restartable。请注意,它可能会将函数标记为以下一种、两种或全部都不是:

  • 可跳过:如果编译器将某个可组合函数标记为可跳过,那么在重组期间,如果该函数的所有实参都等于其之前的值,Compose 就可以跳过该函数。(在 Kotlin 2.0.20 及更高版本中,借助强力跳过模式,所有可重启的可组合函数都会自动变为可跳过。)
  • 可重启:可重启的可组合项充当可开始重组的“范围”。换句话说,该函数可以作为入口点,供 Compose 在状态更改后开始重新执行代码以进行重组。

类型

Compose 会将类型标记为不可变或稳定。每种类型都是以下类型之一:

  • 不可变:如果某个类型的属性值永远不会更改,并且所有方法都是引用透明的,则 Compose 会将该类型标记为不可变。
    • 请注意,所有基本类型都标记为不可变。其中包括 String、Int 和 Float。
  • 稳定:表示一种属性在构造后可以更改的类型。 如果这些属性在运行时发生变化,Compose 会感知到这些变化。

调试稳定性

如果您的应用正在重新组合参数未发生变化的 composable,请先检查是否在每次重新组合时都分配了不稳定类型的新实例(或者,如果停用了强跳过模式,则检查该 composable 是否具有 var 属性或不稳定类型的 val 属性)。

如需详细了解如何在 Compose 中诊断复杂的稳定性问题,请参阅调试稳定性指南。

修复稳定性问题

如需了解如何为 Compose 实现带来稳定性,请参阅修复稳定性问题指南。

摘要

总而言之,您应注意以下几点:

  • 参数:Compose 会确定可组合项的每个参数的稳定性,以决定在重组期间是使用结构相等性 (.equals()) 还是实例相等性 (===) 来比较它们。
  • 立即修复:如果您发现可组合项未被跳过且导致了性能问题,请检查是否在每次传递时重新创建了不稳定的形参的新实例,或者是否使用了 var 属性而不是 State。
  • 编译器报告:您可以使用编译器报告来确定系统对类的稳定性做出了哪些推断。
  • 集合:Compose 认为标准集合接口(List、Set 和 Map)不稳定,因为其底层实现可能是可变的。在强跳过模式(在 Kotlin 2.0.20 及更高版本中默认启用)下,当集合引用未更改时,不稳定的集合仍会使用快速 O(1) 实例相等性 (===) 跳过重组。除非明确需要结构相等性 (O(N) .equals()),否则请避免将大型集合转换为 Kotlinx 不可变集合或使用 @Immutable 或 @Stable 注释持有集合的类,因为在每次重组时比较每个元素可能比重组本身更耗费资源。
  • 其他模块:Compose 会将未运行 Compose 编译器的模块中的类视为不稳定(在强跳过模式下使用 === 进行比较)。如果数据源经常从具有相同值的非 Compose 模块重新实例化小型扁平模型,您可以配置稳定性配置文件,或使用 @Stable 或 @Immutable(其中 .equals() 成本较低)。

深入阅读