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 状态发生变化时会发生什么:
- Compose 会评估是否应重新组合
ContactRow内的代码。 - 它发现
ContactDetails的唯一实参是Contact类型。 - 由于
Contact是不可变的数据类,因此 Compose 可以确定ContactDetails的所有实参均未发生变化。 - 因此,Compose 会跳过
ContactDetails,不会重组它。 - 另一方面,
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()成本较低)。