原作者:twitter.com/vinaygaba
译:FunnySaltyFish
自从 Jetpack Compose 的第一个稳定版本上线以来,已经过去了好几个月 (译注:本文写于2022年4月)。多家公司已经使用了 Compose 来参与构建他们的 Android 应用程序,成千上万的 Android 工程师每天都在使用 Jetpack Compose 。
虽然已经有大量的文档可以帮助开发人员接受这种新的编程模式,但仍有这么个概念让许多人摸不着头脑。它就是Recomposition
,Compose 赖以运作的基础。
重组是在输入更改时再次调用可组合函数的过程。当函数的输入发生更改时,它便会发生。当 Compose 基于新输入进行重组时,它仅调用可能已更改的函数或 lambda,并跳过其余部分。通过跳过所有未更改参数的函数或 lambda,Compose 可以有效地进行重组。
如果您不熟悉此主题,我将在本文中详细介绍 Recomposition
。对于大多数用例,除非传入的参数变了,否则我们不希望重新调用可组合函数(此处从简表示)。Compose 编译器在这方面也非常聪明,当它有足够的可用信息时(例如,所有原始值类型的参数在设计上都是Stable
的),它会尽最大努力来做些对使用者无感的优化;当信息没那么多时,Compose 允许您通过使用 @Stable 和 @Immutable 注解提供元数据,以帮助 Compose 编译器正确做出决定。
从理论上讲,这一切都是有道理的,但是,如果开发人员有办法了解他们的可组合函数是如何重组的,那将大有裨益。这类功能目前呼声很高,不过要使Android Studio 快捷地为您提供此信息,还有一吨的工作要做。如果你像我一样迫不及待,你可能也想知道在能正式上手工具前,要想在 Jetpack Compose 中调试重组,咱可以做些什么。毕竟嘛,重组在性能上起着重要作用——不必要的重组可能会导致 UI 卡顿。
打日志
调试重组的最简单方法是使用良好的 log 语句来查看正在调用哪些可组合函数以及调用它们的频率。这感觉上很直白,但注意这个坑 : 我们希望仅在发生重组时才触发这些日志语句。这听起来像是 SideEffect
的用武之地。SideEffect 是一个可组合的函数,每当成功的 Composition/ Recomposition 后便会被重新调用。Sean McQuillan 编写了如下代码片段,您可以使用它来调试您的重组。这只是一个框架,您可以根据需要进行调整。
class Ref(var value: Int)
// 注意,此处的 inline 会使下列函数实际上直接内联到调用处
// 以确保 logging 仅在原始调用位置被调用
@Composable
inline fun LogCompositions(tag: String, msg: String) {
if (BuildConfig.DEBUG) {
val ref = remember { Ref(0) }
SideEffect { ref.value++ }
Log.d(tag, "Compositions: $msg ${ref.value}")
}
}
实战如下:
@Composable
fun MyComponent() {
val counter by remember { mutableStateOf(0) }
LogCompositions(TAG, "MyComposable function")
CustomText(
text = "Counter: $counter",
modifier = Modifier
.clickable {
counter++
},
)
}
@Composable
fun CustomText(
text: String,
modifier: Modifier = Modifier,
) {
LogCompositions(TAG, "CustomText function")
Text(
text = text,
modifier = modifier.padding(32.dp),
style = TextStyle(
fontSize = 20.sp,
textDecoration = TextDecoration.Underline,
fontFamily = FontFamily.Monospace
)
)
}
在运行此示例时,我们注意到每次计数器的值更改时,两者都会重组。MyComponent``CustomText
在运行时对重组可视化
Google Play 团队是Google首批利用 Jetpack Compose 的内部团队之一。他们与 Compose 团队密切合作,甚至编写了一份case study,描述了他们迁移到 Compose 的经验。该帖子的宝藏之一是他们开发的可视化重组Modifier
。您可以在此处找到修饰符的代码。为了方便,我在下面添加了该代码段;但不要夸我啊,它是由Google Play团队开发的。
/*
* Copyright 2022 Google Inc.
*
* Licensed under the Apache License,Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,software distributed under the
* License is distributed on an "AS IS" BASIS,WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND,either express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.example.android.compose.recomposehighlighter
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.lerp
import kotlin.math.min
import kotlinx.coroutines.delay
/**
* A [Modifier] that draws a border around elements that are recomposing. The border increases in
* size and interpolates from red to green as more recompositions occur before a timeout.
*/
@Stable
fun Modifier.recomposeHighlighter(): Modifier = this.then(recomposeModifier)
// Use a single instance + @Stable to ensure that recompositions can enable skipping optimizations
// Modifier.composed will still remember unique data per call site.
private val recomposeModifier =
Modifier.composed(inspectorInfo = debugInspectorInfo { name = "recomposeHighlighter" }) {
// The total number of compositions that have occurred. We're not using a State<> here be
// able to read/write the value without invalidating (which would cause infinite
// recomposition).
val totalCompositions = remember { arrayOf(0L) }
totalCompositions[0]++
// The value of totalCompositions at the last timeout.
val totalCompositionsAtLastTimeout = remember { mutableStateOf(0L) }
// Start the timeout,and reset everytime there's a recomposition. (Using totalCompositions
// as the key is really just to cause the timer to restart every composition).
LaunchedEffect(totalCompositions[0]) {
delay(3000)
totalCompositionsAtLastTimeout.value = totalCompositions[0]
}
Modifier.drawWithCache {
onDrawWithContent {
// Draw actual content.
drawContent()
// Below is to draw the highlight,if necessary. A lot of the logic is copied from
// Modifier.border
val numCompositionsSinceTimeout =
totalCompositions[0] - totalCompositionsAtLastTimeout.value
val hasValidBorderParams = size.minDimension > 0f
if (!hasValidBorderParams || numCompositionsSinceTimeout <= 0) {
return@onDrawWithContent
}
val (color, strokeWidthPx) =
when (numCompositionsSinceTimeout) {
// We need at least one composition to draw,so draw the smallest border
// color in blue.
1L -> Color.Blue to 1f
// 2 compositions is _probably_ okay.
2L -> Color.Green to 2.dp.toPx()
// 3 or more compositions before timeout may indicate an issue. lerp the
// color from yellow to red,and continually increase the border size.
else -> {
lerp(
Color.Yellow.copy(alpha = 0.8f),
Color.Red.copy(alpha = 0.5f),
min(1f, (numCompositionsSinceTimeout - 1).toFloat() / 100f)
) to numCompositionsSinceTimeout.toInt().dp.toPx()
}
}
val halfStroke = strokeWidthPx / 2
val topLeft = Offset(halfStroke, halfStroke)
val borderSize = Size(size.width - strokeWidthPx, size.height - strokeWidthPx)
val fillArea = (strokeWidthPx * 2) > size.minDimension
val rectTopLeft = if (fillArea) Offset.Zero else topLeft
val size = if (fillArea) size else borderSize
val style = if (fillArea) Fill else Stroke(strokeWidthPx)
drawRect(
brush = SolidColor(color),
topLeft = rectTopLeft,
size = size,
style = style
)
}
}
}
使用此修饰符实际上是直白明了 —— 只需将 recomposeHighlighter
修饰符加到要跟踪其重组的可组合项的修饰符链上即可。修饰符在其附加到的可组合体周围绘制一个框,并使用颜色和边框宽度来表示可组合中发生的重组量。
边框颜色 | 重组次数 |
---|---|
蓝 | 1 |
绿 | 2 |
黄色到红色 | 3+ |
让我们来看看它在实际使用时的样子。我们的示例有一个简单的可组合函数,该函数具有一个按钮,该按钮在单击计数器时递增计数器。我们在两个地方使用recomposeHighlighter
修饰符 -——MyButtonComponent
本身和``MyTextComponent`,它是按钮的内容。
@Composable
fun MyButtomComponent(
modifier: Modifier = Modifier.recomposeHighlighter()
) {
var counter by remember { mutableStateOf(0) }
OutlinedButton(
onClick = { counter++ },
modifier = modifier,
) {
MyTextComponent(
text = "Counter: $counter",
modifier = Modifier.clickable {
counter++
},
)
}
}
@Composable
fun MyTextComponent(
text: String,
) {
Text(
text = text,
modifier = modifier
.padding(32.dp)
.recomposeHighlighter(),
)
}
在运行此示例时,我们注意到按钮和按钮内的文本最初都有一个蓝色的边界框。这很合理,因为这是第一次重组,它对应于我们使用recomposeHighlighter()
修饰符的两个地方。当我们单击按钮时,我们注意到边界框仅围绕按钮内的文本,而不是按钮本身。这是因为 Compose 在重组方面很聪明,它不需要重组整个按钮 —— 只需重组计数器值更改时依赖的那个 Composable 即可。
recomposeHighlighter
实战
使用此修饰符,我们能够可视化可组合函数中如何发生重组。这是一个非常强大的工具,我能想象出基于此拓展的巨大潜力。
Compose编译器指标
前两种调试重组的方法非常有用,并且依赖于观察和可视化。但是,如果我们有一些更确凿的证据来证明Compose编译器如何解释我们的代码,那不是相当nice?这些感觉起来就像魔法一样,毕竟我们经常不知道编译器是否按照我们想要的方式在解释。
事实证明,Compose 编译器确实有一种机制,能给出关于此信息的详细报告。我上个月发现了它,这让我大吃一惊
原文地址:https://blog.csdn.net/weixin_61845324
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。