原文链接:Jetpack Compose Stability Explained | by Ben Trengrove | Android Developers | Medium
©️一切版权归作者所有,本译文仅用于技术交流请勿用于商业用途,未经允许禁止转载,违者后果自负
你是否曾经测量过可组合项的性能并发现它重组的次数比你预期的要多?你可能会问:“难道Compose的意义不就是状态没有发生变化的时候智能地跳过那些重组吗”。或者在阅读代码时,你可能会看到使用了@Stable
或者@Immutable
注释的类,并且想知道这是什么意思?这些概念都可以使用Compose的稳定性(Stability)来解释。在这篇博文中,我们将了解Compose稳定性的实际含义、如何调试它以及你是否应该担心它。
摘要
- Compose 查看可组合项的每个参数的稳定性,以确定在重组期间是否可以跳过它。
- 如果你注意到你的可组合项没有被跳过并且它导致了性能问题,你应该首先检查不稳定的明显原因,例如 var 参数。
- 你可以使用编译器报告来确定所推断的关于你的类的稳定性。
- 像
List
、Set
和Map
这样的集合类总是被确定为不稳定的,因为不能保证它们是不可变的。你可以改用 Kotlinx 不可变集合,或将你的类注释为@Immutable
或@Stable
- 来自未运行 Compose 编译器的module的类始终被确定为不稳定。添加 compose 运行时的依赖,并在你的模块中将它们标记为稳定,或根据需要将类包装在 UI model类中
- 每个可组合项都应该是可跳过的吗?不。
什么是重组(recomposition)?
在讨论稳定性之前,让我们快速回顾一下重组的定义:
重组是当入参发生变化时再次调用可组合函数的过程。当函数的入参发生时,就会发生这种情况。当Compose根据新输入进行重组(recomposition)时,它只会调用可能已更改的函数或lambda,并跳过其余部分。通过跳过所有没有更改参数的函数或 lambda,Compose 可以高效地重组。
注意那里的关键词——“可能”。 Compose 将在快照状态更改时触发重组,并跳过任何未更改的可组合项。重要的是,只有当 Compose 可以确定可组合项的所有参数都没有更新时,才会跳过可组合项。否则,如果 Compose 不能确定所有参数都没有更新时,它总是会在其父可组合项被重组时被重组。如果 Compose 不这样做,可能会导致不能正确触发重组的错误。正确但性能稍差比不正确但性能稍快要好得多。
让我们使用一个显示联系人详细信息的Row
示例:
fun ContactRow(contact: Contact, modifier: Modifier = Modifier) { var selected by remember { mutableStateOf(false) } Row(modifier) { ContactDetails(contact) ToggleButton(selected, onToggled = { selected = !selected }) } }
使用不可变(immutable)对象
首先,假设我们将 Contact
类定义为不可变数据类,因此如果不创建新对象就无法更改它的数值:
kotlin
复制代码
dataclassContact(val name: String, val number: String)
单击ToggleButton
按钮时,我们会更改选择状态。这会触发 Compose 评估是否应重构 ContactRow
中的代码。当涉及到 ContactDetails
可组合项时,Compose 将跳过重新组合它。这是因为它可以看到没有任何参数(在本例中为联系人)发生变化。另一方面,ToggleButton
的输入已更改,因此可以正确重组。
使用可变(mutable)对象
如果我们的 Contact
类是这样定义的呢?
kotlin
复制代码
dataclassContact(var name: String, var number: String)
现在我们的 Contact
类不再是不可变的,它的属性可以在 Compose 不知道的情况下改变。 Compose 将不再跳过 ContactDetails
可组合项,因为该类现在被视为“不稳定”(下文将详细介绍这意味着什么)。因此,只要所选内容发生更改,ContactRow
也将重新组合。
Compose 编译器中的实现
现在我们知道了Compose试图在确认什么(译者:指的是对象的可变性),让我们看看这实际是如何实现的。
首先,这是Compose 文档 (1, 2) 中的一些定义。
方法(Functions)可以可跳过(skippable)或者可重启(restartable):
可跳过(Skippable)——在重组期间调用时,如果所有参数都等于它们之前的值,则 Compose 能够跳过该函数。 。
可重启(Restartable)——此函数可以作为重组作用域(换句话说,此函数可以用作 Compose 可以在状态更改后开始重新执行代码以进行重组的入口点)。
类型可以是不可变(Immutable)的或者稳定(Stable)的
不可变——表示一种类型,其中任何属性的值在构造对象后都不会改变,并且所有方法都是引用透明的。所有基本类型(
String
、Int
、Float
等)都被认为是不可变的。稳定——表示一种类型是可变的,但如果任何公共属性或方法行为会产生与先前调用不同的结果,Compose 运行时将收到通知(译者:虽然对象内部的数值虽然会发生变化,但是这种变化可以被Compose识别)。
当 Compose 编译器在你的代码的编译阶段时,它会查看每个函数和类型并标记任何与这些定义匹配的函数和类型。 Compose 查看传递给可组合项的类型以确定该可组合项的可跳过性(skippability)。重要的是要注意参数不必是不可变(Immutable)的,只要将所有更改通知 Compose 运行时,它们就可以是可变的(译者:即类也可以是稳定的)。对于大多数类型来说,这将是一个没什么意义的约定,但是 Compose 提供了可变类来为你维护这个约定,例如 MutableState
、SnapshotStateMap
/List
/等。因此,将这些类型用于可变属性将允许您的类维护 @Stable 的契约。在实践中,这看起来像下面这样:
@Stable class MyStateHolder { var isLoading by mutableStateOf(false) }
当Compose状态变化时,Compose会在树中读取这些状态对象的点上寻找最近的可组合函数。理想情况下,这将是重新运行尽可能小的代码的直接祖先。正因为这样,重组重启时,如果参数未改变,任何可跳过的函数都将被跳过。让我们重新看看之前的例子:
data class Contact(val name: String, val number: String) fun ContactRow(contact: Contact, modifier: Modifier = Modifier) { var selected by remember { mutableStateOf(false) } Row(modifier) { ContactDetails(contact) ToggleButton(selected, onToggled = { selected = !selected }) } }
代码中,当 selected
发生变化时,距离被读取的状态(stable)最近的重组作用域是ContactRow
。你可能想知道为什么 Row
没有被选为最近的重组作用域?Row
(以及许多其他基础可组合项,如 Column
和 Box
)实际上是一个内联函数(inline function),内联函数不是重组作用域,因为它们在编译后实际上并没有最终成为函数。 因此ContactRow
顺位成为最小的重组范围。因为Contact
被推断为不可变,所以ContactDetails
被标记为可跳过,Compose编译器添加的代码会检查任何可组合项参数已更改。
当contact
保持不变时,ContactDetails
会跳过重组。接下来,点击ToggleButton
,虽然ToggleButton
是可以被跳过的,但是这种情况下就不会被跳过了,因为其中一个参数,selected已经改变了,因此会导致ToggleButton
被重新执行。这会整个重组作用域被重新执行,完成了一次重组。
重组图解:miro.medium.com/v2/resize:f…
你可能会觉得,“这真的很复杂!为什么我需要知道这个?!”答案是,大多数时候你不应该这样做,我们的目标是让编译器优化您自然编写的代码以提高效率。跳过可组合函数是实现这一点的重要因素,但它也需要 100% 安全,否则会导致很难确定的bug。为此,对要跳过的可组合项的要求是很强的。我们正在努力改进编译器对可跳过性的推断,但总会有编译器无法解决的情况。了解在这种情况下跳过可组合项的工作原理可以帮助您提高性能,但只有在您遇到由稳定性(stability)引起的明显的性能问题时才应考虑。如果可组合项是轻量级的或本身仅包含可跳过的可组合项,则不可跳过的可组合项可能根本没有任何效果。(译者:如果不是遇到了很严重的性能问题,或者可组合项很轻量,则不必考虑稳定性带来的问题)
调试稳定性
如何知道你的可组合项是否被跳过?你可以在Layout Inspector中看到它! Android Studio Dolphin 在 Layout Inspector 中包含对 Compose 的支持,它还会显示您的可组合项被重组和跳过的次数。
Layout Inspector中的重组次数
那么,如果你看到你的可组合项没有被跳过,即使它的参数都没有改变,您会怎么做?最简单的方法是检查它的定义,看看它的任何参数是否明显可变。你是否传递了具有 var 属性或 val 属性但具有已知不稳定类型的类型?如果是,那么该可组合项将永远不会被跳过!
但是,当你无法发现任何明显错误时,你会怎么做?