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

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

Compose编译器报告


Compose编译器可以输出其稳定性推断的结果以供检查。结合检查报告,你可以确定哪些可组合项是可跳过的,哪些不是。这篇文章总结了如何使用这些报告,但有关这些报告的详细信息,请参阅 技术文档

⚠️ 警告:只有当你确实遇到与稳定性相关的性能问题时,才应使用此技术。试图让你的整个 UI 都可以跳过是过早优化,可能会导致未来的维护困难。在针对稳定性进行优化之前,请确保你遵循我们关于 Compose 性能的 最佳实践

默认情况下不启用编译器报告。通过使用compiler flag来开启Compose编译器报告,具体设置因项目而异,但对于大多数项目,你可以将以下脚本粘贴到根 build.gradle 文件中。


/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
subprojects {
 tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
  kotlinOptions {
    if (project.findProperty("composeCompilerReports") == "true") {
      freeCompilerArgs += [        "-P",        "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +        project.buildDir.absolutePath + "/compose_compiler"        ]
      }
      if (project.findProperty("composeCompilerMetrics") == "true") {
        freeCompilerArgs += [          "-P",          "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +          project.buildDir.absolutePath + "/compose_compiler"        ]
      }
    }
  }
}

要调试可组合项的稳定性,你可以运行以下task:


./gradlew assembleRelease -PcomposeCompilerReports=true

⚠️ 警告:确保始终在发布版本(release build )上运行它以确保准确的结果。

此任务将输出三个文件。 (包括来自Jetsnack的示例输出)

-classes.txt — 关于此模块中类的稳定性的报告。 案例

-composables.txt — 关于此模块中可组合项的可重启性和可跳过性的报告。案例.

-composables.csv — 上述文本文件的 csv 版本,用于导入电子表格或通过脚本处理。 案例.

如果你改为运行 composeCompilerMetrics 任务,你将获得项目中可组合项数量的总体统计信息和其他类似信息。这在这篇文章中没有涉及,因为它对调试没有那么有用。 打开 composables.txt 文件,你将看到该模块的所有可组合函数,并且每个函数都将标记它们是否可重新启动、可跳过及其参数的稳定性。这是来自Jetsnack 的一个假设示例,它是 Compose 示例应用程序之一。


restartable skippable scheme(“[androidx.compose.ui.UiComposable]”) fun SnackCollection(
   stable snackCollection: SnackCollection
   stable onSnackClick: Function1<Long, Unit>
   stable modifier: Modifier? = @static Companion
   stable index: Int = @static 0
   stable highlight: Boolean = @static true
)

SnackCollection 可组合项完全可重启可跳过稳定。在可能的情况下,这通常是你想要的,尽管远非强制性的(博文末尾有更多详细信息)。

但是,让我们看另一个例子。


restartable scheme(“[androidx.compose.ui.UiComposable]”) fun HighlightedSnacks(
   stable index: Int
   unstable snacks: List<Snack>
   stable onSnackClick: Function1<Long, Unit>
   stable modifier: Modifier? = @static Companion
)

HighlightedSnacks 可组合项是不可跳过的——只要在重组期间调用它,即使它的参数都没有改变,它也会重组。 这是由不稳定的参数snacks引起的。

现在我们来到 classes.txt 文件来检查 Snack 的稳定性。


unstable class Snack {
   stable val id: Long
   stable val name: String
   stable val imageUrl: String
   stable val price: Long
   stable val tagline: String
   unstable val tags: Set<String>
   <runtime stability> = Unstable
}

作为参考,这是 Snack 的声明方式


data class Snack(
    val id: Long,
    val name: String,
    val imageUrl: String,
    val price: Long,
    val tagline: String = "",
    val tags: Set<String> = emptySet()
)

snacks不稳定的。它的绝大部分参数都是稳定的,但tags被认为是不稳定的。但这是为什么呢? Set 看起来是不可变的,它不是 MutableSet。 不幸的是,Set(以及 List 和其他标准集合类,稍后会详细介绍)在 Kotlin 中被定义为接口,这意味着底层实现可能仍然是可变的。例如,你可以写:


val set: Set<String> = mutableSetOf(“foo”)

变量是常量,它声明的类型不是可变的,但它的实现仍然是可变的。 Compose 编译器无法确定此类的不变性,因为它只看到声明的类型,因此将其声明为不稳定的。现在让我们看看如何使它稳定。


让不稳定稳定(Stabilizing the unstable)


不稳定类导致了性能问题时,尝试使其稳定是个好主意。首先要尝试的是让类完全不可变。

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

换句话说,将所有 var 属性设为 val,并将所有这些属性设为不可变类型。

如果你无法实现上述要求,那你将不得不对任何可变属性使用 Compose State。

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

这意味着在实践中,任何可变属性都应该由 Compose 状态支持,例如 mutableStateOf(…)

回到 Snack 示例,该类看起来是不可变的,那么我们如何解决它呢?

你可以采取以下方法:


Kotlinx 不可变集合(Immutable Collections)


Compose 编译器的 1.2 版包括对 Kotlinx Immutable Collections的支持。这些集合保证是不可变的,并且将由编译器推断为不可变的。该库仍处于 alpha 阶段,因此预计其 API 可能会发生变化。你应该评估这对你的项目是否可以接受。

tags的类型改变为下面这种类型可以让Snack稳定


val tags: ImmutableSet<String> = persistentSetOf()


使用Stable或者Immutable注释


根据上述规则,类也可以使用 @Stable 或 @Immutable 进行注释。

⚠️ 警告:非常需要注意的是,这是一个约定,要遵循相应的注解规则。它本身不会使类不可变/稳定。错误地使用注释可能会导致重组失败。

注释一个类会覆盖编译器对你的类的推断,这样它类似于kotlin的!!运算符。你应该非常小心这些注释的使用,因为如果你弄错了,覆盖编译器行为可能会导致你出现无法预料的错误。如果可以在没有注释的情况下使您的类稳定,那么你应该努力以这种方式实现稳定。

正确注释Snack的方式如下:


/* Copyright 2022 Google LLC.
SPDX-License-Identifier: Apache-2.0 */
@Immutable
data class Snack(
  val id: Long,
  val name: String,
  val imageUrl: String,
  val price: Long,
  val tagline: String = "",
  val tags: Set<String> = emptySet()
)

无论选择哪种方法,Snack 类都将被推断为稳定的。

但是,回到 HighlightedSnacks 可组合项,HighlightedSnacks 仍未标记为可跳过:


unstable snacks: List<Snack>

当涉及到集合类型时,参数面临与类相同的问题,List 总是被确定为不稳定的,即使它是稳定类型的集合。 你也不能将单个参数标记为稳定,也不能将可组合项注释为始终可跳过。所以,你可以做什么?同样,也有很多种方法解决这个问题。

使用Kotlinx 不可变集合而不是List


/* Copyright 2022 Google LLC.
  SPDX-License-Identifier: Apache-2.0 */
  @Composable
  private fun HighlightedSnacks(
    index: Int,
    snacks: ImmutableList<Snack>,
    onSnackClick: (Long) -> Unit,
    modifier: Modifier = Modifier
  )

如果你不能使用不可变集合,你可以在最简单的情况下将List包装在带注释的稳定类中,以将其标记为对 Compose 编译器不可变。


@Immutable
    data class SnackCollection(
      val snacks: List<Snack>
    )

然后,你可以将其用作可组合项中的参数类型。


@Composable
      private fun HighlightedSnacks(
        index: Int,
        snacks: SnackCollection,
        onSnackClick: (Long) -> Unit,
        modifier: Modifier = Modifier
      )

在采用其中任何一种方法后,HighlightedSnacks 可组合项现在既可以跳过也可以重新启动。


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 编译器推断一个类是否稳定,前提是它引用的所有非原始类型都被显式标记为稳定的,而且位于Compose 编译器构建的模块中。如果你的数据层(data layer)和UI层(UI layer,)是分开的(这是推荐的方法),这可能是你会遇到的问题。要解决此问题,你可以:

  • 在你的数据层模块上启用 Compose 编译器,或在适当的地方使用 @Stable 或 @Immutable 标记你的类。
  • 这将涉及向数据层添加 Compose 依赖项,你只需要添加Compose运行时的依赖而不用添加Compose-UI依赖。
  • 将你的数据层的类包装在你的UI层的特定包装类中。

同样的问题也会发生在外部module上,除非它们使用的是 Compose 编译器。

这是一个已知的限制,我们目前正在研究针对多模块架构和外部库的更好解决方案。


所有的重组都应该被跳过吗?


不。

追求应用中每个可组合项的完全可跳过性是不成熟的优化。可跳过实际上会增加其自身的少量开销,这可能不值得,如果你确定可重启的开销大于其价值,你甚至可以将可组合项注释为 不可重启。在许多其他情况下,可跳过不会有任何实际好处,只会导致难以维护代码。例如:

  • 不经常重组或根本不重组的可组合项。
  • 只是被称为可跳过但是实际上项目中没有跳过的场景的可组合项。


总结


这篇博文中有很多信息,所以让我们总结一下。

  • Compose 查看可组合项的每个参数的稳定性,以确定在重组期间是否可以跳过它。
  • 如果你注意到你的可组合项没有被跳过并且它导致了性能问题,你应该首先检查不稳定的明显原因,例如 var 参数。
  • 你可以使用编译器报告来确定所推断的关于你的类的稳定性。
  • ListSet 和 Map 这样的集合类总是被确定为不稳定的,因为不能保证它们是不可变的。你可以改用 Kotlinx 不可变集合,或将你的类注释为 @Immutable@Stable
  • 来自未运行 Compose 编译器的module的类始终被确定为不稳定。添加 compose 运行时的依赖,并在你的模块中将它们标记为稳定,或根据需要将类包装在 UI model类中
  • 每个可组合项都应该是可跳过的吗?不。

有关 Compose 性能的更多调试技巧,请查看我们的最佳实践指南I/O talk


相关文章
|
3月前
|
存储 编译器 Kotlin
Compose 类型稳定性注解:@Stable & @Immutable
Compose 类型稳定性注解:@Stable & @Immutable
149 1
|
3月前
|
移动开发 API Android开发
Compose Multiplatform 官宣进入 alpha 阶段
Compose Multiplatform 官宣进入 alpha 阶段
52 0
|
3月前
|
JSON 测试技术 定位技术
【好用的个人工具】在Docker环境下部署Simple mind map思维导图工具
【5月更文挑战第16天】在Docker环境下部署Simple mind map思维导图工具
203 1
【好用的个人工具】在Docker环境下部署Simple mind map思维导图工具
|
3月前
|
Java 编译器 测试技术
深入浅出 Compose Compiler(5) 类型稳定性 Stability
深入浅出 Compose Compiler(5) 类型稳定性 Stability
58 0
深入浅出 Compose Compiler(5) 类型稳定性 Stability
|
3月前
|
Kubernetes Java Docker
多阶段构建:精妙优化Docker镜像大小和性能
在容器化应用的世界中,Docker镜像大小和性能优化是至关重要的。多阶段构建是一项强大的技术,通过精心设计Dockerfile,可以在构建镜像时去除不必要的组件,从而显著减小镜像大小,提高性能。本文章将深入讨论多阶段构建的各个方面,并通过更为丰富和实际的示例代码,帮助大家全面了解和掌握这一重要技术。
|
安全 编译器 Android开发
[译] Compose之稳定性(Stability)的解释(一)
[译] Compose之稳定性(Stability)的解释
197 0
[译] Compose之稳定性(Stability)的解释(一)
|
人工智能 编解码
告诉Stable Diffusion 2.0你不想要什么,生成效果更好:Negative Prompt显奇效(1)
告诉Stable Diffusion 2.0你不想要什么,生成效果更好:Negative Prompt显奇效
321 1
|
人工智能 自然语言处理 机器人
告诉Stable Diffusion 2.0你不想要什么,生成效果更好:Negative Prompt显奇效(2)
告诉Stable Diffusion 2.0你不想要什么,生成效果更好:Negative Prompt显奇效
524 0
|
Python
Hap-Eval:Sentieon团队开发的开源结构变异SV准确率评估工具
Hap-eval基于单倍型 (haplotype) 对两组SV结果进行比较,首先会将比较区块内的SV拼接成单倍型序列,如果SV的结果中有定相信息,在这一步也可以被利用;然后这些单倍型序列被用来建立一个矩阵,进行结果判断。
148 0
Hap-Eval:Sentieon团队开发的开源结构变异SV准确率评估工具
|
存储 运维 负载均衡
社区文章|MOSN 构建 Subset 优化思路分享
MOSN 使用了 Subset 算法作为其标签匹配路由负载均衡的方式。本文主要介绍 Subset 的原理,包括了在超大规模集群下 MOSN 的 Subset 所遇到的一些性能瓶颈与采用的优化算法。
社区文章|MOSN 构建 Subset 优化思路分享