Jetpack Compose 中的常见功用问题

装备(How to set up your Compose add for optimal performance)

首要,看看怎样正确地装备运用,怎样装备运用以测验和评价功用。在评价Compose运用的功用时,一定要保证运用在发布形式下运转而且要启用R8优化功用。

Running in release with R8 enabled

buildTypes {
    release {
        minifyEnabled true
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
}

为什么呢?将运用布置为调试版别时,运转速度会变慢。由于Android运转时会封闭优化功用来改善调试体验。

例如,为了能够逐步检查代码缩减功用不会启用,在调试形式下运转运用时会停用许多优化功用而这些功用关于保证运用无卡顿至关重要。假如你注意到运用呈现功用问题,就应该先检查运用在发布形式下运转是否存在相同的问题,运用或许底子不存在任何问题。现在你已了解怎样装备运用。

咱们来看几个常见问题并讨论一下怎样处理这些问题

榜首,需记住的事情(Caches calculations and allocations)

@Composable
fun ContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    LazyColumn(modifier) {
        // DON't DO THIS
        items(contacts.sortedWith(comparator)) { contact ->
            ...
        }
    }
}

Jetpack Compose 性能提升最佳实践

咱们将讨论一个简单的“通讯录”运用,该运用显现了很多姓名,看看上面这部分代码有什么问题?

每次重组时列表都会从头排序,可组合项或许会十分频频地运转。在编写代码时必须谨记这一点。

在这个示例中,只需有新行呈现在屏幕上,联系人列表就会从头排序。由于当有新行呈现时,LazyList组合的作用域就会失效,Compose会对它进行从头组合,这意味着咱们的一切代码会从头履行。由于排序操作再次作用域内履行,所以每次重组时体系都会调用排序函数。

咱们能够运用remember函数来缓存开销大的操作和分配并保证这些操作或分配仅在需求时运转。

让咱们回到ContactList,咱们能够将排序操作移到remember函数内,将contactssortComparator用作remember函数的键,这将保证其间任一键有改变时列表都会从头排序。

@Composable
fun ContaceList(...) {
    val sortedContacts = remember(contacts, sortComparator) {
        contacts.sortedWith(sortComparator)
    }
    LazyColumn(modifier) {
        items(sortedContacts) { ... }
    }
}

一项更优化的改善方案是,将此排序操作彻底移出Compose并移到ViewModelDataSource中,仅在需求时更改Compose状况,这样就能够将开销尽或许降低至最低。

第二、键(Helps LazyList know what has changeed in a list)

方才咱们优化了联系人的排序操作,现在咱们回到列表可组合项看看是否能够持续改善。

LazyColumn {
    items(contacts) { contact ->
        ...
    }
}

咱们能够向LazyColumn供给更多信息,协助它了解哪些项产生了改变,你知道是什么吗?那便是Key,你能够为LazyList中的项界说一个键,在不供给键的状况下,Compose会运用该项的方位作为键。当项在列表中移动时,这会给功用带来十分晦气的影响,由于该项之后的每一项也会重组。

Use the key parameter to provide a unique key for each item

LazyColumn {
    items(contacts, key = { it.id }) { contact ->
        ...
    }
}

供给键的方式很简单,将keylambda增加到items函数,就能够供给键了。仅有需求注意的是,每个键都必须是仅有的。

现在,项在列表中移动时Compose就会知道哪个项移动了,只需求重组该项即可。

键的运用不只优化了Lazylist而且解锁了很多功用。

第三、衍生更改(Avoids unnecessary recomposition for state changes we need)

接下来,规划人员要求咱们增加一个按钮用于滚回顶部,关键在于他们只期望在列表向下翻滚后才显现该按钮。

借助Compose声明式编程能够很简单实现这一目标,咱们能够增加一个名为showButton的布尔变量,当榜首个可见项索引大于0时,这个变量的值就变为true,咱们将这个变量用作AnimatedVisibility的参数以保证在该按钮显现和消失时能够呈现出很好的淡入淡出作用。

val listState = rememberLazyListState()
LazyColumn(state = listState) {
    //...
}
val showButton = listState.firstVisibleItemIndex > 0
AnimatedVisibilitu(visible = showButton) {
    ScrollToTopButton()
}

Jetpack Compose 性能提升最佳实践

不过,这儿有一个圈套,由于LazyList会在每次翻滚的每一帧都更新listState变量而且咱们会读取listState。所以会引进很多咱们不需求的重组。

关于showButton变量,咱们只关怀榜首个可见索引何时变为非0或变为0,还有另一个与remember相似的Compose函数能够在这儿帮到咱们,那便是derivedStateOf。Compose供给derivedStateOf函数正是为了应对相似的状况

val listState = rememberLazyListState()
LazyColumn(state = listState) { ... }
val showButton by remember {
    derivedStateOf {
        listState.firstVisibleItemIndex > 0
    }
}
AnimatedVisibility(visible = showButton) { ... }

derivedStateOf将承受频频改变的listState,而且仅挑选出咱们所需状况的改变。在本例中,那便是榜首个课件索引大于0的状况。咱们将条件封装在remember derivedStateOf函数中。

现在,咱们仅在此条件实践产生改变时进行重组,也便是在列表向下翻滚后以及回滚到上方后。derivedStateOf能很好地处理这种状况。它会将繁忙的信息流通换为布尔条件,在碰到将状况转换为布尔条件的任何状况时都能够考虑derivedStateOf能否帮上忙。

但也要记住不适合运用derivedStateOf的状况。你不需求在每次创立衍生变量时都运用derivedStateOf。在下面示例中,咱们想知道联系人列表中的项数,你或许会想,由于咱们要衍生状况,所以应该运用derivedStateOf,但这是不合适的,由于这实践上不会滤除任何改变,也便是说,项计数变量需求更新的次数与计数状况的改变次数彻底相同,derivedStateOf用在这儿实践上还会增加开销而且是多余的。

val contacts by viewModel.contacts.observeAsState()
// DON'T
val contactCount = remember {
    derivedStateOf {
        contacts.size
    }
}
// DO
val contactCount = contacts.size

第四,拖延(Read state only when required)

下面咱们将看到的示例与其说是一个问题还不如说以一次机会的失去。规划人员要求咱们用动画呈现界面的布景。咱们期望方框的布景色在青色和海洋红色之间以动画作用不断地来回呈现。

并不推荐这样做,但咱们能够试一下。在Compose中创立这些动画十分简单,这彻底达到了规划人员的要求而且作用好像十分好。但在这儿,咱们能够进行一项潜在的优化,这项优化或许会很难发现。

val color by animateColorBetween(Color.Cyan, Color.Magenta)
Box(Modifier.fillMaxSizd().background(color))

咱们在要求Compose做很多不用要的作业,动画需求这段代码在每一帧都进行重组。

Jetpack Compose 性能提升最佳实践

为了了解为什么在这儿或许没必要组合,咱们首要应该了解Compose的运作方式。Compose包括三个主要阶段,那便是组合、布局和制作

Jetpack Compose 性能提升最佳实践

  • 在榜首阶段,体系会履行可组合函数,在此阶段,体系会创立或更新运用的内容并界说接下来两个阶段将要履行的作业。
  • 在第二阶段,即布局阶段,体系会丈量由组合界说的内容并承认要将这些元素都放在屏幕上的哪些方位。这个阶段要考虑一切修饰符及对其他可组合函数的一切调用。这些函数包括TextRowColumn等,并会在单词传递中丈量和防止一切内容。
  • 最终一个阶段,即制作阶段,体系会宣布实践图形指令将内容制作到运用的画布。这些指令触及的是图元。例如,制作线条、弧形、矩形、图片和文字并在由上一阶段(即布局阶段)承认的方位制作这些图元。

这三个阶段会在它们读取的数据产生改变的每一帧重复履行。可是,假如数据没有改变就能够越过这三个阶段中的一个或多个。

由于在咱们的运用中,动画作用的呈现会使色彩在每一帧都产生改变,因而每一帧也会产生组合,由于咱们只制作不同的色彩,以你最好只需用新色彩从头制作方框而彻底越过组合和布局阶段。

将状况读取移植延迟到需求时是Compose的一个重要概念,延迟读取能够削减需求从头履行的函数。本例便是这样一种状况而且可让咱们彻底越过组合阶段甚至是布局阶段。

在此版别中,咱们运用drawBehind取代backgrounddrawBehind承受在Compose的制作阶段调用的函数实例。由于这是仅有一次读取色彩值。在色彩产生改变时只会制作阶段的结果需求更改。这样,制作阶段就变成仅有需求从头履行的阶段,让Compose能够越过组合和布局这两个阶段

val color by animateColorBetween(Color.Cyan, Color.Magenta)
Box(
    Modifier.fillMaxSize().drawBehind {
        drawRect(color)
    }
)

Jetpack Compose 性能提升最佳实践

这儿的绝妙之处是在函数实例中读取色彩状况,而不是在组合函数中读取。由于函数实例没有改变,因而它读取的变量坚持不变。从组合的角度来讲没有任何改变,因而不需求从头履行此函数。

像这样在函数实例中读取状况并将它作为参数传递很实用。运用这种做法不只能够如本例相同越过一些阶段,还能够削减在状况产生改变时需求从头履行的代码量。充分利用这种做法的一种方式是运用嵌套,由于嵌套能够隐式创立函数实例。

@Composable
fun ContactCart(contact: Contact) {
    MyCard {
        Text("Name: ${contact.name}")
    }
}

举例来说,当联系人的姓名产生改变时,仅会从头履行对Text的调用,对ContactCardMyCard的调用会被越过,由于它们不会读取联系人的姓名,只要对Text的调用会读取,这项调用在函数实例中捕获。由于重组能够从任何组合函数实例的开始从头开端,因而函数实例可用于削减所读取数据产生改变时需求从头履行的代码量

Jetpack Compose 性能提升最佳实践

第五、向后运转(Don’t write to state you have alread read)

在上个实例中,代码能够运转但能够优化,下一个功用问题与之不同,它触及咱们应该一直防止的代码。

var balance by remember { mutableStateOf(0) }
balance = 0
for (transaction in transactions) {
    Row {
        balance += transaction
        Text("Transaction: $transaction Balance: $balance")
    }
}

规划人员要求咱们显现银行买卖列表和响余额,就像银行对账单上显现的相同。为此,咱们核算余额的累计总计并依据每次买卖进行更新,然后显现买卖和新的余额。

可是咱们面对一个问题,在咱们深入讨论之前,你能找出这个问题吗?

当咱们盯梢运用的体系轨道时吗,咱们开端意识到这儿面存在问题,在构建运用的发布版别并保证依据本篇博客最前面进行装备后,咱们运用Android Studio的内置功用分析器在CPU视图中盯梢体系轨道。该轨道便是咱们现在看到的轨道。

Jetpack Compose 性能提升最佳实践

咱们很快注意到运用的主线程比预期要繁忙得多。事实上,咱们期望主线程变为闲置状况由于屏幕底子没有任何改变。

由于Android Studio自动为咱们启用了运用轨道符号,因而咱们能够看到重组符号呈现在每一帧中,咱们能够辨别出它确实呈现在每一帧,由于Android Studio用对比明显的色彩条突出显现了每一帧。现在组合一直在产生好像永久不会停止。

Jetpack Compose 性能提升最佳实践

让咱们回到代码,并试着找出原因,最终证明,问题出在更新余额的代码行,这行代码违反了Compose的中心假定。Compose假定:值一旦被读取,在组合完成之前会坚持不变。你绝不应对已在组合中读取的值进行写入,对已读取的数据进行写入正是咱们所说的向后写入,这违反了Compose的中心假定,或许会导致在每一帧都产生重组,就像本例中的状况相同。

为了更好地了解何时会产生向后写入,咱们再回去看看这段代码。向后写入产生在更新余额这行代码,但读取操作好像产生在Text调用中的写入操作之后,那这怎样会是向后写入呢?

假如咱们想这样展开循环,向后写入就更简单看出来了,在balance读取数据之前向其写入数据没有问题,正是在循环本身中向balance写入数据导致组合一直认为它已过期,需求从头履行,更新以列表中的第二项开始的值是这个问题的底子原因。当组合认为它已过期时它会为下一帧调度一个新组合。假如该组合将它本身符号为已过期,组合将针对下一帧进行自我调度,无休无止。

val balance by remember { mutableStateOf(0) }
balance = 0
transaction = transactions[0]
balance += transaction
Text("Transaction: $transaction Balance: $balance")
transaction = transactions[1]
balance += transaction
Text("Transaction: $transaction Balance: $balance")
transaction = transaction[2]
balance += transaction
Text("Transaction: $transaction Balance: $balance")

这是一个更优的代码版别,该版别再次用到了咱们熟悉的remember函数而且可彻底防止写至状况。

该版别仅在可组合函数初次呈现时,将它履行一次而且仅在买卖产生改变时才被视为已过期。

val balance = remember(transactions) {
    transactions.runningReduce { a, b -> a + b }
}
for ((transaction, balance) in transaction.zip(balance))
    Text("Transaction: $transaction Balance: $balance")

一个比上述版别还要优异的版别是,在ViewModel中核算余额,就像前面讲到的排序示例,在组合开端之前就让这些核算履行。

在进行这些更改后咱们再开盯梢体系轨道,看到了开始预期的结果:主线程一开端很繁忙随后变为闲置状况

Jetpack Compose 性能提升最佳实践

进一步查看轨道符号会发现组合仅在买卖列表显现时运转了一次而且没有再次运转的调度。

Jetpack Compose 性能提升最佳实践

因而请谨记,为了防止向后写入绝不要写至现已读取的状况。

第六、根本装备文件(Optimises startup and other critical paths)

在Android Studio中运转运用时,咱们注意到前几秒钟好像会产生卡顿,但之后,看起来很流畅。

咱们首要检查并承认装备没有问题启用了发布形式和R8优化功用,但这个问题仍然存在,关于这个问题的成因,咱们看到的事即时编译的影响。

从Android Studio运转运用时,经常在发动时呈现功用下降的问题,由于代码需求解译。你的用户极有或许永久发觉不到这种问题。这多亏了咱们的下一项优化,那便是根本装备文件,将根本装备文件增加到运用,有助于加速发动速度、削减卡顿以及进步功用。

但究竟什么是根本装备文件呢?

Compose是一个未绑缚库,因而它允许咱们支撑旧的Android版别和设备,而且能够轻松地运用新功用和bug修正来更新Compose。

咱们不用等待Android晋级就能将这些更改传送给你,可是,这会有一个小缺陷:Android在运用之间共享体系资源包括工具包类和可制作目标,这会加速发动并削减内存耗用。由于Compose是一个未绑缚库,因而它不参加这种共享,只被视为运用的另一部分。

当用户从Play商铺装置运用时,下载到设备的APK包括你的一切代码及与运用捆包的一切库。在发动时,此代码必须由Android运转时解译并被编译为机器代码。这个过程比较耗时,因而会降低功用。

Jetpack Compose 性能提升最佳实践

Play商铺已推出相关功用来改善这种状况,那便是云装备文件。Play商铺会逐步汇总有关在运用发动时运用的类和办法的数据。这些数据只是在发动期间运用所运用代码的列表。

Jetpack Compose 性能提升最佳实践

咱们称这个列表为云装备文件。之后,Play商铺会在其他用户下载你的运用时顺便此装备文件。在Android时,Android运转时会运用这些数据来预编译列出的类和办法。这意味着在发动时解译的代码削减了。假如运用更新较为频频,大约不到两周更新一次,那么用户或许永久无法真实体验到这个优势,每次更新运用时也会被清除现有的云装备文件数据。

Jetpack Compose 性能提升最佳实践

你能够运用根本装备文件自行向Play商铺供给此列表,也便是供给根本。当用户下载你的运用时,Play商铺会包括你的根本装备文件以保证在装置时,一直有可用的装备文件数据。这样,运转时就知道要预编译的内容。

Jetpack Compose 性能提升最佳实践

Compose也顺便自己的根本装备文件,该装备文件默许包括在你的APK中,你或许底子不需求履行任何额定操作就能够感受到根本装备文件的优势,只需求知道它的存在就行了。

但从Android Studio运转运用时,根本装备文件不会包括在内,因而在本地测验中察觉不到这个优势。假如你注意到运用在初次发动时运转缓慢,然后变得越来越快,那么默许根本装备文件很有或许能够处理这个问题。

你能够运用测验库Macrobenchmark将运用装备为在发动时启用根本装备文件。这有助于测验用户从Play商铺装置运用时取得的初次运转体验。

由于咱们现已包括了针对Compose库通过优化的装备文件,因而增加自己的根本装备文件不再是一项功用提高保证,由于装备文件需求调整和优化。假如你承认要增加根本装备文件,请一定要进行测验保证它会切实改善目标。装备问价你的生成和这些优势的测验都是运用Macrobenchmark库完成的。

装备文件得到正确优化后会大幅提高运用的功用,将根本装备文件那增加到咱们的其间一个Compose示例Jetsnack后,发动功用进步了22%,将装备文件增加到Google地图运用后,运用的均匀发动时间缩减了30%,这项改善带来的不只仅是发动功用的提高,将装备文件增加到Play商铺运用后,查找页的其实烘托时间能缩短40%。

Jetpack Compose 性能提升最佳实践

从中可看出增加根本装备文件是提高运用功用的重要举措之一。

如需详细了解怎样生成自己的装备文件以及怎样装备Macrobenchmark来生成和测验装备文件,请参考下面的链接:

goo.gle/baseline-pr…

goo.gle/baseline-pr…

总结

  1. 咱们介绍了怎样装备运用以取得最佳功用。发现功用问题后,首要要检查的是在发动发布形式和R8优化功用后问题是否仍然存在。
  2. 然后介绍了一些常见的过错及修正办法:remember {}LazyList keyderivedStateOf {}、延迟读取、向后写入和根本装备文件。