Jetpack Compose 性能优化参考:编译指标(下)

1,024 阅读7分钟

本文译自:chris.banes.dev/composable-…
原标题:Composable Metrics
译:FunnySaltyFish

本文为文章《如何在 Jetpack Compose 中调试重组》中附录的文章,译者同样进行了翻译。限于译者水平,不免有谬误之可能,如有错误,欢迎指正。
考虑到原文较长,本人分成了上下两部分。这是后一半。前一半见juejin.cn/post/712357…

换条路走

到这时我才重新回去看文档,并开始看第二个建议:

通过将函数标记为 @NonRestartableComposable

乍一看,这个建议更像是一种权宜之计(或末路之策),而不是像第一个建议那样去修复类稳定性。让我们看看注释的文档是怎么说的:

此注解 [防止] 那些允许函数跳过或重启的代码被生成。这对于 直接调用另一个可组合函数、自身几乎不做什么、并且本身不太可能失效的小函数 来说可能是可取的。

如果我们往回想想,我们的目标是让 Composable 可重启 可跳过,所以仅读这个注释并不太够。不过,Compose Metrics 指南提供了更多信息:

如果Composable函数不直接读取任何State变量,那么 [使用这个注解] 是个好主意,[因为] 此重启作用域不太可能被使用。

那么这个注释对我们有帮助吗?是也不是。此注解似乎让 Compose 编译器完全忽略了所有可组合函数的自动重启,从而否定了让我们的函数可重启可跳过 的初衷。我相信这意味着任何状态更改都需要 Compose Runtime 找到祖先重启范围,这就是为什么上面的文档说要避免它们用于读取State的函数。

那么接下来干嘛呢?写就近的 UI Model 类需要添加大量的东西,因此对于很多团队来说,这条路不大可行。不过,我倒确实在 Compose Issue Tracker上找到了一个我非常喜欢的解决方案:允许将函数的参数标记为@Stable. 这将使得开发人员能够对Composable函数的参数强制指定稳定性/不变性,即使对于外部参数类型也是如此:

@Composable
fun AirsInfoPanel(
    @Stable show: TiviShow,
    modifier: Modifier = Modifier,
)

目前,它还不能用

@dynamic 的默认参数表达式

从 Metrics 文档中,要注意的第二件事是@dynamic的默认参数表达式。大量Composable使用默认参数来提供灵活的 API。我最近写了一篇关于 Slot API 的文章,它就依赖于默认参数值:

chris.banes.dev/slotting-in…

默认参数的值可以是可组合的,或者不可组合的。使用可组合代码中的值意味着您正在调用的代码可能是可重启 的,并且返回值可以变化。这就是我们所指的@dynamic默认参数。如果默认参数值是@dynamic,则调用方函数也可能需要重启,这就是应避免意外的@dynamic的原因。

编译指标将非@dynamic参数值称为@static,它可能构成您在composables.txt文件中找到的绝大多数内容。但也一些例外情况下,@dynamic是必要的:

您正在显式读取可观察的dynamic变量

关于这一点,您最常见的情况是在Composable上使用MaterialTheme.blah做默认值。这里我们有一个Composable,它有 3 个被标记为dynamic的参数。

restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun TopAppBarWithBottomContent(
  stable backgroundColor: Color = @dynamic MaterialTheme.colors.primarySurface
  stable contentColor: Color = @dynamic contentColorFor(backgroundColor, $composer, 0b1110 and $dirty shr 0b1111)
  stable elevation: Dp = @dynamic AppBarDefaults.TopAppBarElevation
)

前两个参数backgroundColorcontentColor是dynamic(动态)的,因为我们在间接读取挂在MaterialTheme上的 composition locals . 由于主题是相对静态的(理论上来说),返回值实际上不应该经常改变,所以它是动态的也问题不大。

但是对于elevation参数,我就不大确定为什么它被标记为动态的了。它使用来自 Material 提供的 AppBarDefaults.TopAppBarElevation 属性的值,该属性定义为:

object AppBarDefaults {
    val TopAppBarElevation = 4.dp
}

dp属性被标记为@Stable,并且Dp类被标记为@Immutable。所以从我读到的情况来看,这可能是个bug?

我在另一个函数上也发现了类似的问题:

restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun SearchTextField(
  stable keyboardOptions: KeyboardOptions? = @dynamic Companion.Default
  keyboardActions: KeyboardActions? = @dynamic KeyboardActions()
)

keyboardOptions指的是KeyboardOptions(一个单例) 的伴生对象,并keyboardActions在创建一个新的空KeyboardActions实例,我读着感觉这两个实例都应该被推断为@static

与这篇博文的第一部分类似,我不确定我们在这里可以做些什么来影响 Compose 编译器。我们可以将@Stable和@Immutable添加到我们自己的类中,但从dp上面的示例来看,这似乎并不总是有效。

为啥要在release下?

在这篇博文的开头,我们提到您需要在release版本上启用 Compose Compiler 指标。当您在debug模式下构建应用程序时,Compose 编译器会启用许多功能来加快开发。其中之一是Live Literals,它使 Android Studio 能在不重新编译Composable的情况下“注入”某些参数值的新值。

为了做到这一点,Compose 编译器将某些默认参数替换为另一些生成后的代码。然后,Android Studio 可以调用这些代码来设置新值。最终效果是生成的 Live Literal 代码将导致您的默认参数为@dynamic,即使它们实际上并不是动态的。

您可以在下面看到一个示例。红色(译注:这里没颜色,以 - 开头的行)是debug模式输出,绿色(译注:同,+ 开头的行)来自release构建。release模式下,参数expanded变成了@static

--- debug.txt        2022-04-06 14:43:16.000000000 +0100
+++ release.txt        2022-04-06 14:43:24.000000000 +0100
@@ -1,11 +1,11 @@
 restartable skippable scheme("[androidx.compose.ui.UiComposable, [androidx.compose.ui.UiComposable], [androidx.compose.ui.UiComposable], [androidx.compose.ui.UiComposable]]") fun ExpandableFloatingActionButton(
   stable text: Function2<Composer, Int, Unit>
   stable onClick: Function0<Unit>
   stable modifier: Modifier? = @static Companion
   stable icon: Function2<Composer, Int, Unit>
-  stable shape: Shape? = @dynamic MaterialTheme.shapes.small.copy(CornerSize(LiveLiterals$ExpandingFloatingActionButtonKt.Int$arg-0$call-CornerSize$arg-0$call-copy$param-shape$fun-ExpandableFloatingActionButton()))
+  stable shape: Shape? = @dynamic MaterialTheme.shapes.small.copy(CornerSize(50))
   stable backgroundColor: Color = @dynamic MaterialTheme.colors.secondary
   stable contentColor: Color = @dynamic contentColorFor(backgroundColor, $composer, 0b1110 and $dirty shr 0b1111)
   stable elevation: FloatingActionButtonElevation? = @dynamic FloatingActionButtonDefaults.elevation(<unsafe-coerce>(0.0f), <unsafe-coerce>(0.0f), <unsafe-coerce>(0.0f), <unsafe-coerce>(0.0f), $composer, 0b1000000000000000, 0b1111)
-  stable expanded: Boolean = @dynamic LiveLiterals$ExpandingFloatingActionButtonKt.Boolean$param-expanded$fun-ExpandableFloatingActionButton()
+  stable expanded: Boolean = @static true
 )

我刚刚学到了啥?

到这会儿,您可能会认为您刚刚花了大约 30 分钟,阅读我的文章、光看我指出了许多可能的问题……好吧您大概也是对的 😅。但是我仍然认为这里有一些团队可以采取的行动:

  • 尽早更新到 Compose 的新版本!这将让您能尝试并获得性能上的更新(并报告可能的退步)。
  • 寻找那些被标记为@Composable的小的功能函数或lambda表达式. 这些往往会返回一个值(而不是更新 UI),并且往往只是为了可以引用 composition locals (根据我的经验,LocalContext是一个常见的罪魁祸首)才被标记为Composable。您可以通过传入依赖项轻松地拿掉这个Composable注解。

最后的想法

正如我在上面提到的,我认为这些新指标向前迈进了 ✨ 惊人的 ✨ 一步,可以看到我们的Composable实际被推断出的内容。

我指出的问题实际上是一件好事,并表明这些指标和输出是有效的。如果没有这些,我们将完全不知道会被推断出什么来,也没法看到推断的结果何时不完全符合预期。有了这些信息,在Compose问题跟踪器上创建问题就变得更加容易。

这些指标现在显然非常原始,但在知道 Compose + Android Studio 工具团队有多出色的前提下,我确信一个相应的 Android Studio GUI 版本不会等太久的。我期待着看到团队把让它成为现实!


感谢 Yoali Taylor Nacho Ben 的审阅🙌

(原文结束)


我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿