深入理解 @ReadOnlyComposable、@NonRestartableComposable 和 @NonSkippableComposable

本文翻译自 blog.shreyaspatil.dev/deep-dive-i...,详细探讨了 Compose 中常见的几个概念:可跳过性、可重启性、稳定性等,并分析了 @ReadOnlyComposable@NonRestartableComposable@NonSkippableComposable三个注解的用法,如果你对 Compose 进阶有追求,欢迎阅读

你好,各位 Composers 👋!当我们在浏览不同 Compose 库的内部代码时,经常会遇到 Jetpack Compose 标准库中常用的各种注解。理解它们的含义和用法非常有益,而且如果使用得当 ,这些注解还可以帮助提高 composable 的性能,所以我决定写一篇文章来探讨这个主题。让我们开始吧。

本文旨在揭开三个编译器注解的神秘面纱:@ReadOnlyComposable@NonRestartableComposable@NonSkippableComposable。对于已经熟悉基础知识的 Jetpack Compose 开发者来说,这次探索将提供更清晰的见解,帮助你了解这些注解如何工作、何时使用它们,以及它们如何通过实例来帮助构建更精良、性能更高的应用程序。

⚡ 快速回顾

在深入探讨具体的注解之前,我们有必要重温一下 Jetpack Compose 渲染模型的一些基本概念。这些机制是 Compose 实现其高效性和动态性的核心。

Recomposition (重组)

Recomposition 是当 Composable 函数的底层 state 或输入发生变化时,Jetpack Compose 重新执行这些函数的过程。这是保持 UI 与应用程序数据同步的核心机制。通常,在 Composable 内部读取的 State<T> 对象发生变化会触发其 recomposition。Compose 会精心地跟踪哪些 composable 依赖于哪些 state 对象,以高效地执行这些更新。

Skipping (跳过) - Compose 的智能惰性

为了避免不必要的工作,Compose 具有一个智能的跳过机制。如果一个 composable 的输入自上次执行以来没有发生变化,Compose 就可以跳过重新运行该 composable 及其子项,并重用先前生成的 UI。这是 Compose 性能策略的基石,因为它可以在数据保持不变的情况下,防止整个子树重新渲染。一个 composable 要具备被跳过的资格,必须满足几个条件:其参数必须是稳定的 (stable),它不能有非 Unit 的返回类型,并且不能被 @NonSkippableComposable 注解标记,也不能是一个 non-restartable 的 composable(这本身就对跳过有影响,我们将在本文后面探讨)。

Restartability (可重启性) - 独立的重新调用

Restartable composable 是指那些 Compose 运行时可以在 recomposition 期间独立重新调用的函数。这意味着它们建立了自己的"restart scope (重启作用域) "。如果在一个 restartable composable 内部读取的 state 发生变化,Compose 可以从那个特定的 composable 开始重启执行。相反,如果一个 non-restartable composable 因为其自身参数的变化需要更新,recomposition 过程可能会从其最近的 restartable 父作用域开始。

一个 restartable 的 composable 函数作为一个独特的"作用域",recomposition 过程可以从这里开始。它充当一个特定的入口点,Jetpack Compose 可以从该入口点开始重新执行代码,以响应 state 变化或参数更新。本质上,每个返回 Unit(UI Composable 的典型返回类型)的非内联 (non-inline) composable 函数都会被 Compose 编译器转换以建立这样一个作用域。编译器通过将函数体包装在定义此 restartable 边界的机制中来实现这一点。这种转换包括注入对 startRestartGroup 等函数的调用,并传递额外的参数,如 Composer 实例和 changed 标志,这些在运行时管理这些作用域至关重要。

当一个在 restartable composable 函数体内被读取的 state 对象(例如 MutableState<T>)的值发生变化时,与该 composable 关联的作用域就会被无效化。这种无效化会将该作用域标记为"脏"(dirty),并安排其进行 recomposition。同样,如果传入一个 restartable composable 的参数值与之前的值不同,这也会触发其重组,前提是该 composable 没有因为其他原因被跳过。

看这里可以更好地理解:

图译:

RS_1:MainContent:当 state 的值改变时,此作用域会重启

RS_2:Content:内部没有持有 state,因此仅当 RS1 重组且 state 变化时才会重启

RS_3:ExpandableText:将随 expand 的值变化而重组,也会因 RS_1 重启且 state.longMessage 变化时重组

图例:绿色:可重启、可跳过(由于状态在这些作用域中被读取,重启可由它们开启);黄色:不可重启(内联函数);白色:可重启、可跳过(虽然是可重启的,单也可以被跳过,因为没有直接的状态读取)

所以,在上面的例子中:MainScreenExpandableText 提升 (hoist) 了一个 state,这使得它们具备了自我重启的能力。

Content 也是一个非内联的 composable,所以它形成了自己的 restartable scope ,但是 Content composable 只有在 recomposition 从最近的 restartable scope(例如读取了 state 变化的 MainScreen)开始时才会重组。简而言之,即使 Content 有 restartable scope (RS_2),它也永远不会自己重启。因为它也是可跳过的 (skippable) ,所以如果 state 和上次一样,它会跳过 recomposition。然而,对于 ExpandableText,如果 state.longMessage 改变,recomposition 可以从父级的 restartable scope 开始,即从 MainScreenContentExpandableText;或者它也可以因为读取了 expand state 而自我重组。

此外,由于 Column 在 Compose 中是一个内联函数 (inline-fun),它不会有自己的作用域,而是会使用父级的作用域,即 Content 的作用域。

参数稳定性的作用

参数稳定性是一个契约,它告知 Compose 一个类型的值是否会改变,以及如果改变了,Compose 是否会收到通知。原始类型(如 IntBoolean)、String 和函数类型 (lambda) 本质上被 Compose 编译器认为是稳定的 (stable)。然而,自定义类需要满足特定标准(例如,所有公共属性都是 val 且类型稳定)或被 @Stable@Immutable 等注解明确标记,才能被视为稳定。稳定的输入是启用跳过优化的先决条件。

跳过和可重启性之间的相互作用是微妙的。一个 composable 函数可能是可重启的,意味着它可以作为 recomposition 的独立起点,但如果其输入没有改变,它仍然可以被跳过。相反,一个不可重启的 composable 如果其父级强制它更新,它仍然会进行 recomposition。深刻理解这些核心机制:recomposition、跳过、可重启性和稳定性,是有效利用接下来讨论的高级注解的基础。误解这些基本原理可能导致对这些专门工具的错误应用。

Restartable 和 Skippable 之间的关系

一个 composable 函数可以是 restartable 但不是 skippable。这种情况通常发生在该函数有一个或多个不稳定参数时。在这种情况下,如果其父 composable 触发了 recomposition,这个 restartable 但 non-skippable 的子 composable 也会重新执行其函数体,无论其直接输入从简单相等性的角度看是否发生了变化(因为 Compose 无法信任这些输入的稳定性)。为了获得最佳性能,一个 composable 的理想状态是既 restartable 又 skippable。Restartable 使其能够作为一个独立的 recomposition 单元,而 skippable 则确保这个单元只有在其输入真正改变时才会执行工作。

这里的联系是根本性的:一个 restartable scope 提供了 recomposition 的粒度 ,它定义了"什么可以 被独立重绘 "。而 Skippability(可跳过性),受参数稳定性的严重影响,提供了智能 来决定"它是否应该被重绘 "。一个能力如果缺少另一个,效果就会大打折扣。例如,一个 non-restartable(内联)函数本身不能被跳过;其父级的可跳过性决定了它的命运。像 BoxColumnRow 这样的内联 composable 不会创建自己的作用域;它们是其父作用域的一部分。相反,一个 restartable 但 non-skippable 的函数作为一个明确定义的边界,如果其父级启动了包含它的 recomposition 过程,它将总是重绘。这突显了在 restartable composable 中追求参数稳定性以最大化性能优势的重要性。

概念总结:

参数 描述 在 Recomposition 中的作用 如何实现
Restartable 一个可作为 recomposition 起点的"作用域"或入口的 composable。 使 Compose 能够仅重新执行 UI 树的特定部分。 编译器将大多数非内联、返回 Unit 的 composable 标记为 restartable(例如,通过 startRestartGroup)。
Skippable 一个在 recomposition 期间,如果其输入未改变就可以跳过执行的 composable。 避免不必要的工作,提高性能。 所有输入必须是稳定的且未改变(通过 equals 比较)。编译器根据参数稳定性进行标记。不适用于返回非 Unit 的函数。

这个对比框架有助于阐明这些不同但相关的概念是如何协同工作以实现高效的 recomposition。


🪧 注解

1. @ReadOnlyComposable: 只读不写 UI

@ReadOnlyComposable 注解是一个标记,用于那些只打算从当前 composition 上下文中读取信息,并且绝不能生成任何 UI 节点@Composable 函数。这类函数可能会访问 CompositionLocal 的值(如 LocalContext.current 或主题属性)或基于 composition 环境计算值。它与编译器建立了一个关于该函数只读 UI 输出性质的契约。

它有何帮助?

通过保证不写入任何 UI 节点,@ReadOnlyComposable 允许编译器执行某些优化。在检查编译后的代码时,被此注解标记的函数通常不包括用于生成 UI 元素的 composable 所标准的 startReplaceableGroupendReplaceableGroup 调用。这直接减少了为那些不向 UI 树贡献节点的函数管理 slot table(译注:Composition 内部存储的机制,可见 juejin.cn/post/711373...)的开销。除了优化,它还清楚地表明了函数的用途:这是一个为在 composition 内进行数据检索或计算而设计的 composable,而不是用于 UI 构建。这促进了更清晰的关注点分离,使得依赖于 composition 的数据检索逻辑(例如,当前主题、屏幕密度)可以存在于生成 UI 的 composable 之外,从而增强了可重用性和可测试性。

示例:

  1. 访问 MaterialTheme 属性:

    kotlin 复制代码
       object MaterialTheme {
           val colors: Colors
               @Composable
               @ReadOnlyComposable
               get() = LocalColors.current // LocalColors.current 是一个 CompositionLocal
    ​
           val typography: Typography
               @Composable
               @ReadOnlyComposable
               get() = LocalTypography.current // LocalTypography.current 是一个 CompositionLocal
       }

    MaterialTheme.colors 的 getter 需要是 @Composable 才能访问 LocalColors.current。然而,它本身不绘制任何 UI;它仅仅返回 Colors 对象。@ReadOnlyComposable 注解告知编译器这一特性。

  2. 访问诸如字符串或尺寸之类的资源:

    kotlin 复制代码
    @Composable
    @ReadOnlyComposable
    fun localizedGreeting(userName: String): String {
        // stringResource 本身也是 @ReadOnlyComposable
        val greetingFormat = stringResource(R.string.greeting_format)
        return String.format(greetingFormat, userName)
    }
    ​
    @Composable
    @ReadOnlyComposable
    fun screenPadding(): Dp {
        return dimensionResource(R.dimen.screen_padding)
    }

    解释: 这些工具函数利用 composable 上下文(通过 stringResourcedimensionResource,它们本身也是 @ReadOnlyComposable)来获取值。它们不生成 UI,但为其他 composable 提供数据。

何时使用它:

  • 对于需要访问 CompositionLocal(例如 LocalContext.current, LocalDensity.current, LocalLayoutDirection.current)但不渲染 UI 的工具函数。
  • 用于主题属性访问器,如 MaterialTheme 示例中所示。
  • 用于任何基于 compositional 信息计算并返回值,然后该值将被其他生成 UI 的 composable 使用的函数。

重要约束: 一个关键规则是,@ReadOnlyComposable 函数只能调用其他同样标记为 @ReadOnlyComposable@Composable 函数。试图从一个 @ReadOnlyComposable 函数内部调用一个常规的、生成 UI 的 composable 会导致编译时错误。这个限制对于维护"不生成 UI"契约的完整性至关重要。如果一个 @ReadOnlyComposable 可以调用一个生成 UI 的 composable,编译器就无法再保证 @ReadOnlyComposable 本身不会间接导致 UI 生成,从而使潜在的优化失效。此外,这些函数不应引入(写入)副作用或持有 State,以免触发其他 UI 元素的 recomposition。在 @ReadOnlyComposable 函数内部使用 remember 也可能存在问题,因为 remember 与 composer 交互以在 slot table 中存储值,从 composer 的角度来看,这并非严格的"只读"操作,可能会违反该注解的契约。


2. @NonRestartableComposable - 控制 Recomposition 边界

@NonRestartableComposable 注解应用于 @Composable 函数,以向 Compose 编译器表明,它不应生成允许此函数在 recomposition 期间独立重启或跳过的常规机制。实质上,它告诉 Compose 这个特定的 composable 不需要自己的"restart scope"。Restart scope 允许 Compose 在仅局部 state 改变时,只重新执行 UI 树的特定部分,而无需重新执行其父级。

简单来说:@NonRestartableComposable 告诉 Compose 编译器,某个特定的 composable 函数不需要能够独立重启 。相反,如果其父 composable 重组,它将总是随之重组。这可以节省管理其可重启性相关的微小开销。

它有何帮助?

这个注解可以在特定的、有限的场景下作为微优化。对于那些作为另一个单一 composable 的直接包装器、内部逻辑极少、并且本身不太可能被无效化(即因其自身参数变化或直接读取 state 而重组)的小型、简单的 composable 函数,它可能是有益的。通过将这样的函数标记为 non-restartable,编译器避免了为其分配一个 restart scope,这在这些特定情况下可以使 composition 过程的成本略微降低。如果 Compose 编译器报告显示一个函数是 restartable 但不是 skippable(可能是由于一个或多个难以使其稳定的不稳定参数),将其标记为 @NonRestartableComposable 是两个建议的优化路径之一。另一条路径是通过确保其所有参数都稳定来使函数变为 skippable。

示例

kotlin 复制代码
@NonRestartableComposable // 如果 'content' 很少独立变化,则可能是候选
@Composable
fun SimpleIconWrapper(icon: ImageVector, modifier: Modifier = Modifier) {
    // 逻辑非常少,主要将参数传递给 Icon。
    // 假设 'icon' 和 'modifier' 是稳定的,并且不经常变化。
    Icon(imageVector = icon, contentDescription = null, modifier = modifier)
}
​
// 与直接读取 state 的 composable 对比:
@Composable
fun UserProfileHeader(userState: State<User>) {
    // 这个 composable 读取 'userState'。如果 'userState.value' 改变,
    // 这个 composable 需要重组。
    // @NonRestartableComposable 在这里可能不合适,因为它
    // 预期会成为其自身 state 变化的 recomposition 的根。
    Text(text = userState.value.name)
    //... 其他用户详情
}

在这里,SimpleIconWrapper 除了调用 Icon composable 之外几乎不做任何事情。如果它的参数(icon, modifier)是稳定的,并且很少以需要 SimpleIconWrapper 本身成为 recomposition root 的方式改变,那么它可能是一个候选。关键是它"不太可能刷新自身"。

何时考虑使用它?

  • 简单且无状态: 它不使用 remember 来管理自己的内部 state。它主要依赖于传递给它的参数。适用于那些主要委托给另一个单一 composable、内部逻辑极少、并且不太可能成为 recomposition root 的小型、无状态函数(即它们不直接读取频繁变化的 state,并且它们的参数是稳定的)。
  • (可能)频繁使用: 如果该 composable 被实例化很多次,例如在长列表或复杂 UI 中,其好处更有可能变得明显(尽管通常仍然很微小),因为每个实例节省的开销可能会累加起来。
  • 叶子节点或薄包装器: 它通常作为 UI 树中的叶子节点出现,或者作为另一个 composable 的非常简单的包装器,应用固定的修饰符或简单的转换。
  • 当编译器报告显示一个函数是 restartable 但不是 skippable,并且通过稳定所有参数来使其 skippable 不切实际或不理想时,此注解提供了一种替代的优化策略。

重要考量和注意事项:

  • 微优化: 这是为了微调性能。实际的收益通常非常小,在大多数应用中可能不会被注意到。
  • 先做性能分析: 在应用此类优化之前,始终使用性能分析工具(如 Android Studio 的 Layout Inspector 或 recomposition tracking)来识别实际的性能瓶颈。过早的优化会使代码更难阅读和维护,而收益微乎其微。
  • UI 不正确的风险: 如果你错误地假设一个 composable 不需要是 restartable(即,在某些情况下它应该被跳过,但现在因为其父级重组而不会被跳过),这可能导致不必要的 recomposition,甚至在父级重组但子级的特定输入(本应导致跳过)未改变时导致不正确的 UI。
  • 不适用于复杂逻辑: 如果一个 composable 具有复杂的逻辑,或者真正能从独立跳过中受益,它应该保持 restartable。

对 recomposition scope 的影响

如果一个 @NonRestartableComposable 函数确实需要重组(例如,因为它的一个参数改变了),recomposition 将由其在 composable 树中最近的 restartable 祖先作用域发起。这可能导致比该函数拥有自己专用 restart scope 时更大范围的 UI 树重组。这种权衡是其使用的核心:节省微小的作用域分配开销,换来的是在该 composable 本身变化时可能更广泛的 recomposition。这使其理想用例变得相当狭窄,通常适用于稳定的透传包装器。如果该 composable 具有显著的内部逻辑或多个具有自身潜在 state 读取的子 composable,那么为其自身的 restart scope 付出开销可能是合理的,以实现更细粒度的 recomposition。


3. @NonSkippableComposable - 强制重新执行

@NonSkippableComposable 注解确保一个 composable 函数在其父 composable 重组时总是会被执行(重组),即使它自己的所有输入参数都是稳定的并且自上次 composition 以来没有改变。它有效地让一个 composable 选择退出 Compose 的正常跳过机制。

它有何帮助?

在默认跳过行为不可取的特定情况下,此注解很有用。

  • 当一个 composable 具有重要的副作用或内部逻辑,并且必须在其父级的每个 recomposition 周期中重新执行时,可以使用它。
  • 如果需要一个 composable 是 restartable 但明确 non-skippable,它可作为一种机制来选择退出"强跳过 (strong skipping)"模式。强跳过在较新的 Compose 版本中默认启用,使得更多的 composable 可跳过;@NonSkippableComposable 是明确的覆盖选项。
  • 它也可以作为调试工具,以确保某个特定的 composable 在 recomposition 周期中确实按预期被调用。
kotlin 复制代码
@NonSkippableComposable
@Composable
fun DebuggableCounterDisplay(count: Int, label: String) {
    // 这个 composable 如果其父级重组,将总是执行其函数体,
    // 无论 'count' 或 'label' 是否已更改。
    Log.d("RecompositionLogger", "DebuggableCounterDisplay executed with: $label - $count")
    Text("Label: $label, Count: $count (我总是和我的父级一起重组!)")
}
​
@Composable
fun ControllingParent() {
    var parentStateTrigger by remember { mutableStateOf(0) }
    val stableCount = 5
    val stableLabel = "Current Value"
​
    Button(onClick = { parentStateTrigger++ }) {
        Text("强制父级重组: $parentStateTrigger")
    }
​
    // 尽管 'stableCount' 和 'stableLabel' 不会改变,
    // 但每次 'ControllingParent' 因为 'parentStateTrigger' 变化而重组时,
    // 'DebuggableCounterDisplay' 都会重新执行(并打印日志)。
    DebuggableCounterDisplay(count = stableCount, label = stableLabel)
}

在这种情况下,每当 ControllingParent 进行 recomposition 时,DebuggableCounterDisplay 都会打印其日志消息并重绘其 Text 组件。由于 @NonSkippableComposable 注解的存在,无论 stableCountstableLabel 的值是否实际改变,这都会发生。这代表了一种为这个特定 composable 优先执行而非优化的刻意选择。

何时使用它(谨慎地):

  • 对于包含绝对必须在每次父级 recomposition 时运行的关键副作用的 composable。然而,重要的是要批判性地评估这些副作用是否最好由专门的副作用处理器(如 LaunchedEffectDisposableEffectSideEffect)来管理。这些处理器对生命周期和取消提供了更细粒度的控制,仅仅为了一个副作用而强制整个 composable 重新运行可能不够清晰。
  • 用于调试目的,以确认某个特定的 composable 在 recomposition 周期中被调用。
  • 当强跳过模式启用时(Kotlin 2.0.20+ 中默认),并且有特定需求需要一个 restartable 的 composable 是 skippable。

性能影响: 此注解有意地绕过了 Compose 的一个核心优化(可跳过性)。因此,过度使用它可能导致性能下降,因为 composable 会执行比严格必要时更多的工作。虽然 skippable 的 composable 可能会为跳过逻辑生成更多的代码,但 non-skippable 的 composable 会产生重新执行的运行时成本。


ReadOnlyComposable 理解起来相当直接,但 @NonRestartableComposable@NonSkippableComposable 有些不同,可能会让人困惑。所以让我们更好地理解它们之间的区别。

NonRestartableComposableNonSkippableComposable 的主要区别

虽然 @NonRestartableComposable@NonSkippableComposable 都影响 recomposition 行为,并且都被列为可以使 composable 不符合标准跳过条件的因素,但它们作用于过程的不同方面。它们之间的混淆可能源于两者都代表了对"正常" composable 行为的偏离。然而,它们偏离默认跳过路径的原因是不同的。

@NonRestartableComposable 主要影响 composable 函数是否建立其自己的 restart scope 。Restart scope 允许 Compose 在其直接输入或读取的 state 改变时,只重新执行该 composable 及其子项。而 @NonSkippableComposable 则直接规定了当其父级重组时,即使其输入保持不变,该 composable 的执行是否可以被跳过

关键的是,@NonRestartableComposable 并不 意味着 @NonSkippableComposable。一个 composable 函数可以是 non-restartable(意味着如果其自身参数改变,它依赖于其父级的 restart scope),但如果其自身参数稳定且在该父作用域发起 recomposition 时没有改变,它仍然可以是 skippable。相反,一个 composable 可以是 restartable(拥有自己的作用域),但被标记为 @NonSkippableComposable 以确保它总是随其父级重新执行。

下表提供了并排对比,以阐明它们各自的特点:

注解 @NonRestartableComposable @NonSkippableComposable
主要效果 阻止 composable 拥有自己独立的 restart scope。 阻止 composable 的执行被跳过,即使输入未改变。
如果输入改变 最近的 restartable 父作用域发起包含此 composable 的 recomposition。 如果其父级重组,该 composable 总是重新执行。
如果输入不变(且父级重组) 如果其参数稳定且未改变,仍然可以被跳过。 总是重新执行。
目标 通过避免 restart scope 分配,为简单的包装器进行微优化。 为副作用、调试或为 restartable composable 明确选择退出强跳过而强制执行。
与强跳过的交互 保持 non-skippable(因为强跳过适用于 restartable 的 composable)。 明确使一个 restartable 的 composable 变为 non-skippable,覆盖强跳过的默认行为。
典型用例 具有稳定输入的小型、无状态包装函数。 调试;特定的副作用(谨慎使用);选择退出强跳过。
性能影响 节省 restart scope 的微小开销;如果它发生变化,可能导致更广泛的 recomposition。 故意产生重新执行的成本;绕过核心优化。

在这些注解之间进行选择,需要清楚地理解为什么 需要改变默认行为。目标是为了一个微不足道的、稳定的函数节省微小的作用域开销(@NonRestartableComposable),还是为了确保一个函数无论其输入如何都总是运行(@NonSkippableComposable)?将一个误用于另一个的预期目的,可能导致微不足道的收益,或者更糟的是,意想不到的性能问题。

与强跳过模式 (Strong Skipping Mode) 的交互

强跳过模式,在 Kotlin 2.0.20 及更高版本的 Compose 编译器中默认启用,显著改变了 composable 的默认可跳过性行为。此模式代表了 Compose 优化策略的一个显著转变,旨在使更多的 composable 开箱即用即可跳过,从而减轻开发者仅为实现跳过而手动确保参数稳定性的负担。

强跳过模式引入的关键变化是:

  1. 具有不稳定参数的 Composable 变得可跳过: 在强跳过模式下,即使一个 composable 函数接收到编译器无法推断为稳定的类型的参数,该函数仍然可以被跳过。对这些不稳定参数的比较是使用实例相等性 (===) 完成的。
  2. 捕获了不稳定变量的 Lambda 会被记住/记忆化: 编译器会自动将 lambda 表达式(即使是那些捕获了不稳定变量的)包装在 remember 调用中,并根据捕获的稳定性使用适当的 key。本质上,通过强跳过,所有 restartable 的 composable 函数默认都变得 skippable

@NonSkippableComposable 作为退出选项

在强跳过环境中,@NonSkippableComposable 注解变得尤为重要。如果有一个 restartable 的 composable 不应该 被跳过(尽管强跳过倾向于使其可跳过),@NonSkippableComposable 是强制其重新执行的明确方式。这为开发者提供了在必要时精确控制、覆盖激进的默认跳过行为的能力。没有它,如果强跳过启发式不适用于某个需要保证执行的特定 restartable composable,开发者将束手无策。

@NonRestartableComposable 和强跳过

即使启用了强跳过,使用 @NonRestartableComposable 注解的函数仍然是 non-skippable 的。其原理是,强跳过主要修改的是 restartable 函数可以被跳过的条件。由于一个 @NonRestartableComposable 函数根据定义缺乏自己独立的 restart scope,强跳过的规则在这种情况下并不会从根本上改变其 non-skippable 的性质。它的 non-skippable 性质本质上与其缺乏 restart scope 相关,而不仅仅是参数稳定性的考量。


结论

注解 @NonRestartableComposable@ReadOnlyComposable@NonSkippableComposable 是 Jetpack Compose 开发者工具箱中的强大工具。它们提供了对 Compose 编译器和运行时的细粒度控制,实现了超越默认功能的优化和行为契约的强制执行。尽管如此,这些注解常常会令人困惑,所以强烈建议在实现它们之前,通过性能分析和评估来正确使用它们 ,并且只在充分理解的情况下使用这些注解。这种方法可以增强你对实现的信心。使用诸如 Android Studio 中的 Layout Inspector(检查重组次数)、用于更广泛性能测试的 Jetpack Macrobenchmark 以及 Compose 编译器报告(分析稳定性和可跳过性)之类的工具

我希望你已经了解了这些注解在 Jetpack Compose 中究竟是如何工作的。

太棒了。希望你从中获得了一些有价值的见解。如果你喜欢这篇文章,请分享它 😉,因为...

"分享即是关怀"

谢谢你!😄

X 上联系我或访问我的网站以了解更多关于我的信息 😎。

原文完毕,本文解释的还是非常深刻的。下一篇文章我们将探讨 Compose Lazy List 中一些正在进行的优化手段,比如预拉取、可暂停&恢复的子任务等,敬请期待~

相关推荐
咖啡の猫8 小时前
Android开发-常用布局
android·gitee
程序员老刘8 小时前
Google突然“变脸“,2026年要给全球开发者上“紧箍咒“?
android·flutter·客户端
Tans59 小时前
Androidx Lifecycle 源码阅读笔记
android·android jetpack·源码阅读
雨白9 小时前
实现双向滑动的 ScalableImageView(下)
android
峥嵘life9 小时前
Android Studio新版本编译release版本apk实现
android·ide·android studio
studyForMokey11 小时前
【Android 消息机制】Handler
android
敲代码的鱼哇11 小时前
跳转原生系统设置插件 支持安卓/iOS/鸿蒙UTS组件
android·ios·harmonyos
翻滚丷大头鱼11 小时前
android View详解—动画
android
我是好小孩12 小时前
[Android]RecycleView的item用法
android
胖虎112 小时前
Android Studio 读取本地文件(以 ZIP 为例)
android·ide·android studio·本地文件·读取本地文件