[译] Compose之稳定性(Stability)的解释(一)

简介: [译] Compose之稳定性(Stability)的解释

原文链接Jetpack Compose Stability Explained | by Ben Trengrove | Android Developers | Medium

©️一切版权归作者所有,本译文仅用于技术交流请勿用于商业用途,未经允许禁止转载,违者后果自负


你是否曾经测量过可组合项的性能并发现它重组的次数比你预期的要多?你可能会问:“难道Compose的意义不就是状态没有发生变化的时候智能地跳过那些重组吗”。或者在阅读代码时,你可能会看到使用了@Stable或者@Immutable注释的类,并且想知道这是什么意思?这些概念都可以使用Compose的稳定性(Stability)来解释。在这篇博文中,我们将了解Compose稳定性的实际含义、如何调试它以及你是否应该担心它。


摘要


  • Compose 查看可组合项的每个参数的稳定性,以确定在重组期间是否可以跳过它。
  • 如果你注意到你的可组合项没有被跳过并且它导致了性能问题,你应该首先检查不稳定的明显原因,例如 var 参数。
  • 你可以使用编译器报告来确定所推断的关于你的类的稳定性。
  • ListSetMap 这样的集合类总是被确定为不稳定的,因为不能保证它们是不可变的。你可以改用 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)的

不可变——表示一种类型,其中任何属性的值在构造对象后都不会改变,并且所有方法都是引用透明的。所有基本类型(StringIntFloat 等)都被认为是不可变的。

稳定——表示一种类型是可变的,但如果任何公共属性或方法行为会产生与先前调用不同的结果,Compose 运行时将收到通知(译者:虽然对象内部的数值虽然会发生变化,但是这种变化可以被Compose识别)。

当 Compose 编译器在你的代码的编译阶段时,它会查看每个函数和类型并标记任何与这些定义匹配的函数和类型。 Compose 查看传递给可组合项的类型以确定该可组合项的可跳过性(skippability)。重要的是要注意参数不必是不可变(Immutable)的,只要将所有更改通知 Compose 运行时,它们就可以是可变的(译者:即类也可以是稳定的)。对于大多数类型来说,这将是一个没什么意义的约定,但是 Compose 提供了可变类来为你维护这个约定,例如 MutableStateSnapshotStateMap/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(以及许多其他基础可组合项,如 ColumnBox)实际上是一个内联函数(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 的支持,它还会显示您的可组合项被重组和跳过的次数。

image.png

Layout Inspector中的重组次数

那么,如果你看到你的可组合项没有被跳过,即使它的参数都没有改变,您会怎么做?最简单的方法是检查它的定义,看看它的任何参数是否明显可变。你是否传递了具有 var 属性或 val 属性但具有已知不稳定类型的类型?如果是,那么该可组合项将永远不会被跳过!

但是,当你无法发现任何明显错误时,你会怎么做?


相关文章
|
缓存 API Android开发
安卓现代化开发系列——传世不朽ViewModel
安卓现代化开发系列——传世不朽ViewModel
356 0
|
编译器 API 容器
Compose:从重组谈谈页面性能优化思路,狠狠优化一笔
Compose:从重组谈谈页面性能优化思路,狠狠优化一笔
652 0
|
存储 Java API
淘宝拍立淘图片搜索接口:轻松找到同款商品!
淘宝拍立淘图片搜索接口:轻松找到同款商品!
|
弹性计算 运维 数据挖掘
DDD与微服务架构浅析
主要介绍DDD,微服务架构,以及两者之间的关系
DDD与微服务架构浅析
|
6月前
|
数据采集 人工智能 关系型数据库
《深度破局:构建MySQL数据治理框架,赋能AI项目腾飞》
在数据驱动的时代,AI项目的成功高度依赖于高质量的数据。MySQL作为广泛使用的关系型数据库,其数据治理框架的构建至关重要。本文从AI对数据质量的要求出发,探讨了在MySQL环境中制定数据治理策略(如数据标准、元数据管理和质量目标)、优化治理流程(如数据采集、清洗与存储)以及明确组织架构和职责划分的重要性。同时,强调了数据安全与隐私保护的底线,包括访问控制和加密技术的应用。通过全方位的数据治理,可为AI项目提供坚实的数据基础,推动技术创新与业务发展。
185 27
|
12月前
|
Web App开发 前端开发 安全
前端研发链路之测试
本文由前端徐徐撰写,介绍了前端测试的重要性及其主要类型,包括单元测试、E2E测试、覆盖率测试、安全扫描和自动化测试。文章详细讲解了每种测试的工具和应用场景,并提供了选择合适测试策略的建议,帮助开发者提高代码质量和用户体验。
270 3
前端研发链路之测试
|
机器学习/深度学习 XML 搜索推荐
图像自动化保存工具:Python脚本开发指南
图像自动化保存工具:Python脚本开发指南
IBSS、BSS和ESS之间的区别
【8月更文挑战第23天】
1249 0
|
编译器 API
Compose:长期副作用 + 智能重组 = 若智?(一)
Compose:长期副作用 + 智能重组 = 若智?
237 0
|
存储 人工智能 运维
首批 I 阿里云通过算力服务成熟度增强级评估
近日,阿里云作为算力服务标准主要参编单位之一,参与了首批标准符合性验证,以阿里云飞天企业版为主要参评产品,完成了通用计算、智能计算和高性能计算三类计算服务能力的符合性评估。
809 1