Jetpack Compose 中的重组作用域和性能优化

news/2024/5/18 20:10:16/文章来源:https://blog.csdn.net/lyabc123456/article/details/129232729

只有读取可变状态的作用域才会被重组

这句话的意思是只有读取 mutableStateOf() 函数生成的状态值的那些 Composable 函数才会被重新执行。注意,这与 mutableStateOf() 函数在什么位置被定义没有关系。读取操作指的是对状态值的 get 操作。也就是取值的操作。

从一个最简单的例子开始:

@Composable
fun Sample() {Column(modifier = Modifier.padding(4.dp).shadow(1.dp, shape = CutCornerShape(topEnd = 8.dp)).background(getRandomColor()).padding(4.dp),horizontalAlignment = Alignment.CenterHorizontally) {var counter by remember { mutableStateOf(0) }Text("Text1", color = Color.White, modifier = Modifier.background(getRandomColor()))Button(modifier = Modifier.fillMaxWidth(),colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),onClick = { counter++ },shape = RoundedCornerShape(5.dp)) {Text("Text2: counter: $counter", color = Color.White, modifier = Modifier.background(getRandomColor())) }}
}
fun getRandomColor() =  Color(red = Random.nextInt(256),green = Random.nextInt(256),blue = Random.nextInt(256),alpha = 255
)

在上面的代码中,我们为每个 Composable 组件都设置了一个随机的背景颜色,这样,一旦它们发生了重组,我们就可以观察到。

在这里插入图片描述

这里点击 Button 修改 counter 的值之后,只有读取 counterText 组件背景色发生变化,这充分的说明了只有这个 Text 组件才会重组。位于 Button 之上的 Text 组件,虽然它与 counter 定义在同一作用域范围内,但是它不会被触发重组,因为它没有读取counter 的值。

假如我们把 Button 内的组件换成一个自定义的 Composable 组件,只要它读取 counter 的值,那么该自定义组件的整个作用域范围都会执行重组:

@Composable
private fun Sample1() {Column(modifier = Modifier.padding(4.dp).shadow(1.dp, shape = CutCornerShape(topEnd = 8.dp)).background(getRandomColor()).padding(4.dp),horizontalAlignment = Alignment.CenterHorizontally) {var counter by remember { mutableStateOf(0) }Text("Text1", color = Color.White, modifier = Modifier.background(getRandomColor()))Button(modifier = Modifier.fillMaxWidth(),colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),onClick = { counter++ },shape = RoundedCornerShape(5.dp)) { MyText(counter)}}
}@Composable
fun MyText(counter: Int) {Column {Text("MyText: counter: $counter", color = Color.White, modifier = Modifier.background(getRandomColor()))Text("Another Text", color = Color.White, modifier = Modifier.background(getRandomColor()))}
}

在这里插入图片描述

可以看到,点击修改counter 值的时候,不仅 MyText 组件中的第一个读取 counter 值的 Text 组件会发生重组,而且 MyText 组件中的另一个未读取 counter 值的 Text 组件也发生了重组。也就是说整个 MyText 组件都发生了重组。

内联组件的重组作用域与其调用者相同

在一般情况下,读取某个state值的组件和未读取某个state值的组件,它们的重组作用域是隔离的,互不影响。但是内联组件除外。可以通过下面的例子来说明这个问题:

@Composable
fun Sample() {Column(modifier = Modifier.padding(4.dp).shadow(1.dp, shape = CutCornerShape(topEnd = 8.dp)).background(getRandomColor()).padding(4.dp)) {var update1 by remember { mutableStateOf(0) } println("ROOT")Text("Text in outer Column", color = Color.White, modifier = Modifier.background(getRandomColor()))Button(modifier = Modifier.fillMaxWidth(),colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),onClick = { update1++ },shape = RoundedCornerShape(5.dp)) {println("🔥 Button 1")Text(text = "Text in Button1 read update1: $update1",textAlign = TextAlign.Center,color = Color.White,modifier = Modifier.background(getRandomColor()))}Column(modifier = Modifier.padding(4.dp).shadow(1.dp, shape = CutCornerShape(topEnd = 8.dp)).background(getRandomColor()).padding(4.dp)) {println("🚀 Inner Column")var update2 by remember { mutableStateOf(0) }Button(modifier = Modifier.fillMaxWidth(),colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),onClick = { update2++ },shape = RoundedCornerShape(5.dp)) {println("✅ Button 2")Text(text = "Text in Button2 read update2: $update2",textAlign = TextAlign.Center,color = Color.White,modifier = Modifier.background(getRandomColor()))}Column(modifier = Modifier.padding(4.dp).shadow(1.dp, shape = CutCornerShape(topEnd = 8.dp)).background(getRandomColor()).padding(6.dp)) {println("☕ Bottom Column")/*** 🔥🔥 Observing update(mutableState) causes entire composable to recompose*/Text(text = "🔥 Text in Inner Column read update1: $update1",textAlign = TextAlign.Center,color = Color.White,modifier = Modifier.background(getRandomColor()))}} }
}

在这里插入图片描述

当我们点击 Button 2 时,只会影响读取 update2 值的范围,这没有问题。但是我们点击 Button 1 时,整个组件都在为我们闪烁!这并没有像我们预想的那样:只影响 Button 1 中的读取 update1Text 以及内部嵌套 Column 中读取 update1Text 组件。而是影响了整个外部的 Column 组件的作用域。

这是因为 Column 组件是被定义为 inline 内联的。我们可以通过它的源码定义中发现:

@Composable
inline fun Column(modifier: Modifier = Modifier,verticalArrangement: Arrangement.Vertical = Arrangement.Top,horizontalAlignment: Alignment.Horizontal = Alignment.Start,content: @Composable ColumnScope.() -> Unit
) {val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)Layout(content = { ColumnScopeInstance.content() },measurePolicy = measurePolicy,modifier = modifier)
}

所以在上面的例子中,最外层的 Column 组件嵌套了第二个 Column 组件,而第二个 Column 组件嵌套了第三个 Column 组件,由于inline函数的特性,最终所有 Column 组件内部的组件都会被直接在编译期插入到最外层中。所以第三个 Column 组件中读取 update1Text 实际上相当于是处在最外层中。因此当 update1 发生变化时,整个最外层都会重组,因为它们属于同一个重组作用域。

同样的,Row 组件也是内联的。因为这两个组件是在开发当中是会高频使用的组件,所以我们要尤其注意这一点。如果我们不想某个状态值导致整个组件都重组,换句话说,如果我们想最大程度的做到状态隔离,缩小重组作用域,那么最好使用非 inline 的组件,例如 Box 组件、Surface 组件等等。

隔离重组作用域

上面提到, inline 组件会将重组作用域暴露给调用者,进而导致调用者的重组作用域被放大,子组件发生重组时父组件也受到了牵连。但是,假如我们的业务代码中已经大量的应用了 ColumnRow 这样的内联组件,或者我们此时想提升一下页面渲染的性能,想要追求极致的用户体验,我们该怎么办呢?换句话说,就是 如何隔离重组作用域? 其实很简单,说出来你可能不信:既然 inline 的不行,那么改成 inline 的不就可以了嘛。

比如上面的例子,我们可以将 Column 组件换成一个自定义的 Column,我们只需要简单地在外层包装一个 Composable 函数透传 content 即可。例如像下面这样:

@Composable
fun RandomColorColumn(content: @Composable () -> Unit) { Column(modifier = Modifier.padding(4.dp).shadow(1.dp, shape = CutCornerShape(topEnd = 8.dp)).background(getRandomColor()).padding(4.dp)) { content()}
}

然后我们只需将上面例子中的 Column 全部换成这个 RandomColorColumn ,而其他部分的代码基本不动:

@Composable
fun Sample() {RandomColorColumn {var update1 by remember { mutableStateOf(0) } Text("Text in outer Column", color = Color.White, modifier = Modifier.background(getRandomColor()))Button(modifier = Modifier.fillMaxWidth(),colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),onClick = { update1++ },shape = RoundedCornerShape(5.dp)) { Text(text = "Text in Button1 read update1: $update1",textAlign = TextAlign.Center,color = Color.White,modifier = Modifier.background(getRandomColor()))}RandomColorColumn { var update2 by remember { mutableStateOf(0) }Button(modifier = Modifier.fillMaxWidth(),colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),onClick = { update2++ },shape = RoundedCornerShape(5.dp)) { Text(text = "Text in Button2 read update2: $update2",textAlign = TextAlign.Center,color = Color.White,modifier = Modifier.background(getRandomColor()))}RandomColorColumn {   Text(text = "🔥 Text in Inner Column read update1: $update1",textAlign = TextAlign.Center,color = Color.White,modifier = Modifier.background(getRandomColor()))}} }
}

效果:

在这里插入图片描述

现在,事情就会变得跟我们预期的那样:当疯狂点击 Button 1 时,只有 Button 1 中的读取 update1Text 以及内部嵌套 RandomColorColumn 中读取 update1Text 组件会发生重组,而不是影响所有整个组件。

也许有人会问:这相比原来直接使用 Column,你又多了嵌套一层,不会影响性能吗?答案是:并不会。原因主要有两点:

  • 之前在 Jetpack Compose中的绘制流程和自定义布局 中提到过,Compose 中不允许被多次测量,每个子元素只允许被测量一次,因此并不会因为嵌套层级的增加而导致测量次数的指数爆炸问题,正所谓 “一时嵌套一时爽,一直嵌套一直爽”。
  • 实际上 Compose 编译器会对 Composable 函数施加一些 “魔法”,而 Compose runtime 会持有对这些 Composable 函数的引用,它们可能在运行时以任意顺序被重新执行、并行执行(可能多线程)、甚至被跳过执行,所以它们并不像我们传统意义上的标准函数调用堆栈那样,调用顺序也不会跟我们代码书写的那样按照先后顺序一层一层的往下调用再返回。这一点在 Jetpack Compose 深入探索系列一:Composable 函数 中有介绍过,如果你感兴趣的话可以自行了解。

当然嵌套多了的话,也不能说完全没有影响,至少会增加 Compose 编译器的编译时间成本,还有就是最终生成的DEX包可能会大一些。

另外,Jetpack Compose 这个框架本身已经极为优秀了,正常情况下也不会出现太大的性能问题,一般也不需要这么做。只有在你想要鸡蛋里挑骨头、追求极致性能体验的情况下,才需要十分小心的留意你所使用的 Composable 组件是否是 inline 的。

重组作用域内不读取任何参数的组件不会被重组

这里表达的意思是某个组件不从外部接受参数,当我们定义一个组件时,可以在组件内部维护一些状态值,也有可能通过状态提升,将一些状态作为参数暴露出来,交给其公共父组件来管理。但是在父组件中,一旦发生重组,它只会影响那些会为其传递参数的子组件,如果某个子组件不从父组件接受任何参数,那么它在重组中保持不变(从父组件的视角)。这貌似跟本文列出的第一点有点重复,但是我还是想列出来单独说一下。

我们再利用一下本文最开头的例子进行一下修改:

@Composable
fun Sample() {Column(modifier = Modifier.padding(4.dp).shadow(1.dp, shape = CutCornerShape(topEnd = 8.dp)).background(getRandomColor()).padding(4.dp),horizontalAlignment = Alignment.CenterHorizontally) {var counter by remember { mutableStateOf(0) }Text("Text1 Read counter: $counter", color = Color.White, modifier = Modifier.background(getRandomColor()))Button(modifier = Modifier.fillMaxWidth(),colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),onClick = { counter++ },shape = RoundedCornerShape(5.dp)) { Text("Text2 Don't Read counter", color = Color.White, modifier = Modifier.background(getRandomColor())) }}
}

在这里插入图片描述
我们发现 Button 中的 Text 组件不会发生变化,这是因为它没有读取 counter 的状态值。但是我们发现 Button 本身的颜色却在发生变化,说明它正在发生重组,这是因为 Column 组件是内联的,这一点我们上面已经介绍过,因此 ButtonButton 之上的那个 Text 组件实际上在调用 Sample() 的组件中,是处于相同的重组作用域。所以这里我们就会看到有趣的事: Button 会重组,但它内部的组件却不一定,这取决于其内部组件是否会从外部接受参数,在这个例子中并没有。

现在,我们来修改一下代码,让 Button 中的 Text 组件接受一些外部的参数:

@Composable
fun Sample() {Column(modifier = Modifier.padding(4.dp).shadow(1.dp, shape = CutCornerShape(topEnd = 8.dp)).background(getRandomColor()).padding(4.dp),horizontalAlignment = Alignment.CenterHorizontally ) {var counter by remember { mutableStateOf(0) }val myData = remember { MyData() } Text("Text1 Read counter: $counter", color = Color.White, modifier = Modifier.background(getRandomColor()))Button(modifier = Modifier.fillMaxWidth(),colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),onClick = {counter++myData.value = myData.value + 1 },shape = RoundedCornerShape(5.dp)) { Text("Text2 Read myData.value: ${myData.value}", color = Color.White, modifier = Modifier.background(getRandomColor())) }}
}
class MyData(var value: Int = 0)

再次运行查看效果:

在这里插入图片描述

这里让 Button 中的 Text 2 读取的是一个普通的类 MyData 中的字段值,并且特意没有设置成 mutableStateOf(),只使用了 remember 函数, remember 充其量只是一个缓存函数(只有第一次执行初始化操作),也就是说 myData并不是一个能被 Composable 观察的 state 值。

但是我们发现了比较神奇的事情: Button 中没有读取任何可变状态的 Text 2 竟然也发生了重组!因为MyData 这个类会被编译器推断为具有不稳定性,而不确定性的东西是会破坏Compose runtime的智能重组的,所以Compose就干脆放弃这种情况,每次都会调用它重新执行组合。严格来讲, Text 2 组件发生重组是由于 Text 1 组件读取 counter组件导致的,如果你把 Text 1 组件注释掉,那么Text 2 组件就不会发生重组(如果还会发生重组,那就是见鬼了)。

或者,我们可以更简单一点,不要用什么 MyData 类了,就直接放一个普通的变量让 Text 2 去读取:

@Composable
fun Sample() {Column(modifier = Modifier.padding(4.dp).shadow(1.dp, shape = CutCornerShape(topEnd = 8.dp)).background(getRandomColor()).padding(4.dp),horizontalAlignment = Alignment.CenterHorizontally ) {var counter by remember { mutableStateOf(0) }var myVal = 0Text("Text1 Read counter: $counter", color = Color.White, modifier = Modifier.background(getRandomColor()))Button(modifier = Modifier.fillMaxWidth(),colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),onClick = {counter++myVal++},shape = RoundedCornerShape(5.dp)) { Text("Text2 Read myVal: $myVal", color = Color.White, modifier = Modifier.background(getRandomColor()))}}
} 

在这里插入图片描述

你会发现虽然 Text 2 组件读取的 myVal 的值没有变化,但是 Text 2 组件的背景色却在闪,说明它仍然在发生重组。

既然如此,那我们可再简化一点,直接让 Text 2 组件什么也不读取了,但是我们在它上面一行加一个 println 函数,打印一下myVal的值:

@Composable
fun Sample() {Column(modifier = Modifier.padding(4.dp).shadow(1.dp, shape = CutCornerShape(topEnd = 8.dp)).background(getRandomColor()).padding(4.dp),horizontalAlignment = Alignment.CenterHorizontally ) {var counter by remember { mutableStateOf(0) }var myVal = 0Text("Text1 Read counter: $counter", color = Color.White, modifier = Modifier.background(getRandomColor()))Button(modifier = Modifier.fillMaxWidth(),colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),onClick = {counter++myVal++},shape = RoundedCornerShape(5.dp)) { println(myVal)Text("Text2 Read Nothing", color = Color.White, modifier = Modifier.background(getRandomColor())) }}
} 

在这里插入图片描述

这时发现 Text 2 组件的背景色仍然在闪,说明它仍然在发生重组。奇怪了,不是说 Compose 智能重组只会重组那些读取状态值发生改变的组件吗???

是不是很纳闷,那我们再改一下,直接让 println 函数打印一个固定的字符串:

@Composable
fun Sample() {Column(modifier = Modifier.padding(4.dp).shadow(1.dp, shape = CutCornerShape(topEnd = 8.dp)).background(getRandomColor()).padding(4.dp),horizontalAlignment = Alignment.CenterHorizontally ) {var counter by remember { mutableStateOf(0) }var myVal = 0Text("Text1 Read counter: $counter", color = Color.White, modifier = Modifier.background(getRandomColor()))Button(modifier = Modifier.fillMaxWidth(),colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),onClick = {counter++myVal++},shape = RoundedCornerShape(5.dp)) { println("Hello World")Text("Text2 Read Nothing", color = Color.White, modifier = Modifier.background(getRandomColor())) }}
} 

在这里插入图片描述

这下 Text 2 是真的不会变了。

这说明了什么?Button 组件受到重组时,其内部只要读取了外部参数就会发生重组,这里的内部是指 Button { } 大括号中传入的名为 content 的 Composable lambda 参数:

在这里插入图片描述

前面的例子中在Text组件的前后去打印外部的参数,Text 组件也会重组是因为 打印外部参数 这件事实际是直接发生在 content 这个 Composable 组件的内部,也就是说 content 这个 Composable 组件读取了外部的参数。

也就是说 Buttoncontent lambda中只要读取了外部参数就会触发 content 的重组,我们修改成下面这样, Text 2 组件也是会发生重组,因为虽然 Text 2 组件没有读取,但是相邻的组件读取了,而它们都最终都会导致 content lambda 从外部读取参数。(注意下面的代码中,即便你将 Button 内部的 Column 套上Box等非inline的组件也不能隔离重组)

@Composable
fun Sample() {Column(modifier = Modifier.padding(4.dp).shadow(1.dp, shape = CutCornerShape(topEnd = 8.dp)).background(getRandomColor()).padding(4.dp),horizontalAlignment = Alignment.CenterHorizontally ) {var counter by remember { mutableStateOf(0) }var myVal = 0Text("Text1 Read counter: $counter", color = Color.White, modifier = Modifier.background(getRandomColor()))Button(modifier = Modifier.fillMaxWidth(),colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),onClick = {counter++myVal++},shape = RoundedCornerShape(5.dp)) {  Column {Text("Text2 Read Nothing", color = Color.White, modifier = Modifier.background(getRandomColor()))Text("Text3 Read myVal: $myVal", color = Color.White, modifier = Modifier.background(getRandomColor()))}}}
} 

因为 Buttoncontent 内容我们是可以自定义的,也就是说我们可以像下面这样定义:

 Button(modifier = Modifier.fillMaxWidth(),colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),onClick = { counter++ }, ) { SomeComposable() }

这样可能更能表达上面的意思,因为在实际中,向外暴露 content Composable lambda 的自定义组件是很常见的。这种情况下,在 SomeComposable() 中的任何子组件都不会因为 Button 的重组而发生重组,因为它没有从外部接收任何参数。假如我们希望上面的例子中, Text 3 组件读取外部参数的情况下,Text 2 组件不要受到影响,那么解决方法就是将其包装进一个 不接受任何参数SomeComposable() 组件中:

@Composable
fun Sample() {Column(modifier = Modifier.padding(4.dp).shadow(1.dp, shape = CutCornerShape(topEnd = 8.dp)).background(getRandomColor()).padding(4.dp),horizontalAlignment = Alignment.CenterHorizontally ) {var counter by remember { mutableStateOf(0) }var myVal = 0Text("Text1 Read counter: $counter", color = Color.White, modifier = Modifier.background(getRandomColor()))Button(modifier = Modifier.fillMaxWidth(),colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),onClick = {counter++myVal++},shape = RoundedCornerShape(5.dp)) {  Column {SomeComposable()Text("Text3 Read myVal: $myVal", color = Color.White, modifier = Modifier.background(getRandomColor()))}}}
} 
@Composable
private fun SomeComposable() { Column(modifier = Modifier.padding(4.dp).shadow(1.dp, shape = CutCornerShape(topEnd = 6.dp)).background(getRandomColor()).padding(4.dp)) {Text("Text2 Read Nothing", color = Color.White, modifier = Modifier.background(getRandomColor()))}
}

在这里插入图片描述

优化 Modifier 的状态读取

一般来说,应该尽量避免在 Modifier 修饰符上读取状态,因为 Modifier 并不是用来显示数据元素的地方,并且从修饰符上频繁读取状态值会导致频繁的重复进而导致过渡重绘。但有时我们可能避免不了,比如想要位置偏移、大小、背景或边距等根据某个状态来不断变化,从而达到某种效果。甚至有可能会经常这么干,一旦你有这样的需求,官方为我们提供了一些更好的原则值得去遵守。

总的来说就是一条原则:尽量使用带lambda的修饰符方法来读取Modifier需要的状态值。

这是因为使用 lambda意味着延迟读取(Deffered Reading),而不是在组合期间读取(这样会反复触发重组,而重组意味着触发一些列的测量流程,从布局到绘制)。标准的Modifier修饰符方法是一定会在组合期间被执行的,而lambda形式的Modifier修饰符基本上可以确定是不会在在组合期间被执行的,Compose的官方团队就这一点也已经给出了明确的说明。

下面是使用 Modifier.offset(offset: Dp)Modifier.offset(offset: Density.() -> IntOffset) 的一个例子,它很好的表达了这两者的不同效果:

@Composable
fun PhasesSample() { var offsetX by remember { mutableStateOf(0f) }Row(verticalAlignment = Alignment.CenterVertically) {Text(text = "OffsetX")Spacer(modifier = Modifier.width(5.dp))Slider(value = offsetX,valueRange = 0f..50f,onValueChange = {offsetX = it})}val modifier1 = Modifier .offset(x = offsetX.dp) // 直接读.layout { measurable, constraints ->val placeable: Placeable = measurable.measure(constraints)layout(placeable.width, placeable.height) {println("😃️ modifier1 LAYOUT")placeable.placeRelative(0, 0)}}.background(Blue400).drawWithContent {println("😜 modifier1 DRAW")drawContent()}val modifier2 = Modifier .offset { // 放在 lambda 中返回val newX = offsetX.dp.roundToPx()IntOffset(newX, 0)}.layout { measurable, constraints ->val placeable: Placeable = measurable.measure(constraints)layout(placeable.width, placeable.height) {println("🍏 modifier2 LAYOUT")placeable.placeRelative(0, 0)}}.background(Blue400).drawWithContent {println("🍎 modifier2 DRAW")drawContent()}MyBox(modifier = modifier1, "modifier1")Spacer(modifier = Modifier.height(8.dp))MyBox(modifier = modifier2, "modifier2")
}
@Composable
private fun MyBox(modifier: Modifier, title: String) {LogCompositions(msg = "🔥 MyBox() COMPOSITION $title")Column(modifier) {// This Text changes color in every recompositionText(text = title,color = Color.White,modifier = Modifier.background(getRandomColor()).fillMaxWidth().padding(2.dp))Text(text = "modifier hash: ${modifier.hashCode()}\nModifier: $modifier",color = Color.White,modifier = Modifier.heightIn(max = 200.dp),fontSize = 12.sp)}
}

运行效果:

在这里插入图片描述

上面代码中,modifier1 使用标准版本的 offset() 来设置 x 轴的偏移量,而 modifier2 使用 lambda 版本的 offset{} 来设置 x 轴的偏移量。为了观察这两个修饰符在各个布局绘制阶段的执行情况,还为它们设置了layoutdrawWithContent,它们只用来打印 Log,并没有做额外的事情,代码也只是执行原本的逻辑。同时为了观察Modifier的状态,还在 MyBox 组件中将ModifierhashCode和其属性值显示到Text组件上。

最终我们看到的结果就是,使用标准版本 offset()modifier1 背景一直在闪烁,也就是一直在发生重组,并且其hashCode在不停的变化,而使用 lambda 版本 offset{}modifier2 背景不变即没有在发生重组,且其hashCode也保持不变。

同时,通过观察 Logcat 控制台的输出,可以发现:

使用标准版本 offset()modifier1 在不断的执行:COMPOSITION -> LAYOUT -> DRAW 三个阶段

在这里插入图片描述

使用 lambda 版本 offset{}modifier2 只会输出:LAYOUT 这一个阶段。

在这里插入图片描述

结合这些结果,下面再从组合树的角度看一下,为啥标准版不好,而 lambda 大法好:

在这里插入图片描述

标准版本 offset() 在每次偏移量发生改变时,会重新创建 Modifier 实例hashCode不停变化证实了这一点),当 Modifier 实例发生变化时,组合树会先删除旧的,然后再添加一个新的 Modifier 实例。组合树的变化会导致重组的发生,因此如果偏移量频繁变化,重组就会频繁的发生,而每次重组都有可能触发组合->布局->绘制这三个阶段,这非常要命。

在这里插入图片描述

lambda 版本 offset{} 的优势是, Modifier 实例不会改变hashCode保持不变证实了这一点),Compose 只会在需要的时候 调用 lambda 函数,因此组合树不会发生变化,也就是说相比之下可以跳过不必要的重组。(前面的Log输出证实了其只会执行Layout阶段)

除了跳过重组的好处, lambda 版本还有一点好处就是它可能会跳过布局的一半(在大小没有变化的情况下),因为我们知道Compose的布局阶段包含测量和摆放两个阶段,如果只修改了位置,尺寸大小没有变化,那么Compose可以完全可以只执行摆放阶段。因此在最好的情况下,它能为我们节省整个流程的一半的时间。😃️

在这里插入图片描述

如果我们有机会使用 Modifier.layout() (一般是在自定义组件时), 也可以直接在其layout(){}方法中摆放组件的时候进行设置,这会直接发生在布局阶段的摆放阶段。例如将上面例子中的modifier1修改代码如下:

val modifier1 = Modifier  .layout { measurable, constraints ->val placeable: Placeable = measurable.measure(constraints)layout(placeable.width, placeable.height) {println("😃️ modifier1 LAYOUT")// placeable.placeRelative(0, 0)placeable.placeRelative(offsetX.dp.roundToPx(), 0) // 在这里设置}}.background(Blue400).drawWithContent {println("😜 modifier1 DRAW")drawContent()}

这时再运行发现二者都安静了,两个修饰符都不会随offsetX变化而发生重组:

在这里插入图片描述

对于背景色,也是一样的道理,可以使用 Modifier.drawWithContent{} 这个修饰符中绘制背景框,请参考下面的例子:

@Composable
fun PhasesSample() { var index by remember { mutableStateOf(0f) } Text(text = "bgColor")Slider(value = index,valueRange = 0f..100f,onValueChange = { index = it })val modifier1 = Modifier.layout { measurable, constraints ->val placeable: Placeable = measurable.measure(constraints)layout(placeable.width, placeable.height) {println("😃️ modifier1 LAYOUT") placeable.placeRelative(0, 0)}}.drawWithContent {println("😜 modifier1 DRAW")drawContent()}.background(randomColors[index.roundToInt()%randomColors.size]) // 直接设置val modifier2 = Modifier.layout { measurable, constraints ->val placeable: Placeable = measurable.measure(constraints)layout(placeable.width, placeable.height) {println("🍏 modifier2 LAYOUT")placeable.placeRelative(0, 0)}} .drawWithContent { // 在 lambda 中设置val color = randomColors[index.roundToInt()%randomColors.size]println("🍎 modifier2 DRAW color: $color")drawRect(color)drawContent()}MyBox(modifier = modifier1, "modifier1")Spacer(modifier = Modifier.height(20.dp))MyBox(modifier = modifier2, "modifier2") 
}@Composable
private fun MyBox(modifier: Modifier, title: String) {LogCompositions(msg = "🔥 MyBox() COMPOSITION $title")Column(Modifier.background(purple500)) { Text(text = title,color = Color.White,modifier = modifier.fillMaxWidth().padding(4.dp))Text(text = "modifier hash: ${modifier.hashCode()}\nModifier: $modifier",color = Color.White,fontSize = 12.sp)}
}

运行效果:

在这里插入图片描述

同样的,这里使用标准版background()直接设置背景色,会导致Modifier实例不断变化,而使用 Modifier.drawWithContent{} 方法绘制背景色,Modifier实例就会保持不变。如果观察log输出,会发现前者不断输出 COMPOSITIONDRAW (因为位置大小没有变,所以跳过了布局 LAYOUT 阶段),而后者只会输出 DRAW ,也就是说在 lambda 中进行绘制只会影响绘制阶段,它可以为我们跳过组合和布局两个阶段。不得不说,这样大大滴好呀。😃️

看完背景色的设置,再来看一下 padding,奇怪的是在 Compose 的API中我并没有找到关于 padding修饰符的 lambda 版本,这让我很是不解,我就搞不懂了,既然要搞,官方为啥不搞一套呢?难道又需要在Modifier.layout(){}中设置吗?那岂不是很麻烦?其实我们可以自己搞一个 lambda, 因为 lambda 就是一个函数类型的函数参数而已,所以我们可以这样做:

@Composable
fun DeferredPaddingComposablesSample() {var padding by remember { mutableStateOf(0f) }//...PaddingDeferred {padding.dp} 
}
@Composable
private fun PaddingDeferred(padding: () -> Dp) { Text(text = "PaddingDeferred",modifier = Modifier.padding(start = padding()).fillMaxWidth().background(getRandomColor()) )
}

这样虽然不能避免读取 padding 值本身的组件的重组,但是却能够尽量的避免父组件的重组,将重组范围缩小到最小。请看下面的示例:

@Composable
fun DeferredPaddingComposablesSample() {var padding by remember { mutableStateOf(0f) }Row(verticalAlignment = Alignment.CenterVertically) {Text(text = "Padding")Spacer(modifier = Modifier.width(5.dp))Slider(value = padding,valueRange = 0f..50f,onValueChange = {padding = it}) } PaddingOuterDeferred { padding.dp } // 使用 lambda 传PaddingOuterDirectly(padding.dp) // 直接传
}
@Composable
private fun PaddingOuterDeferred(padding: () -> Dp) {Column(modifier = Modifier.fillMaxWidth().background(getRandomColor()).padding(10.dp)) {LogCompositions(msg = "😍 PaddingOuterDeferred")Text("PaddingOuterDeferred")PaddingMiddleDeferred(padding)}
}
@Composable
private fun PaddingMiddleDeferred(padding: () -> Dp) {Column(modifier = Modifier.fillMaxWidth().background(getRandomColor()).padding(10.dp)) {LogCompositions(msg = "😃 PaddingMiddleDeferred")Text("PaddingMiddleDeferred")PaddingInnerDeferred(padding)}
}
@Composable
private fun PaddingInnerDeferred(padding: () -> Dp) {LogCompositions(msg = "😜 PaddingInnerDeferred")Text(text = "PaddingInnerDeferred",modifier = Modifier.padding(start = padding()).fillMaxWidth().background(getRandomColor()))
}@Composable
private fun PaddingOuterDirectly(padding: Dp) {Column(modifier = Modifier.fillMaxWidth().background(getRandomColor()).padding(10.dp)) {LogCompositions(msg = "PaddingOuterDirectly")Text("PaddingOuterDirectly")PaddingMiddleDirectly(padding)}
}
@Composable
private fun PaddingMiddleDirectly(padding: Dp) {Column(modifier = Modifier.fillMaxWidth().background(getRandomColor()).padding(10.dp)) {LogCompositions(msg = "PaddingMiddleDirectly")Text("PaddingMiddleDirectly")PaddingInnerDirectly(padding)}
}
@Composable
private fun PaddingInnerDirectly(padding: Dp) {LogCompositions(msg = "PaddingInnerDirectly")Text(text = "PaddingInnerDirectly",modifier = Modifier.padding(start = padding).fillMaxWidth().background(getRandomColor()))
}

同样的,为了对比效果,这里我们搞了两套代码,一套直接传参数dp值不使用函数类型,另一套传递函数类型。

运行效果:

在这里插入图片描述

对比效果很明显:不使用 lambda 直接传 dp 值的,所有父组件都在发生重组,而使用 lambdadp 值的只有最里面的子组件才会重组。

但是感觉还不是很完美,假如我们想做到像 Modifier.offset{ } lambda 版本那样的效果,也就是读取 padding状态值的时候完全不发生重组,难道就没有办法了吗?当然不是,首先 我们可以使用 Modifier.layout(){}来自己处理,这样比较麻烦,但是可以实现。还有更简单一点的办法:自定义 Modifier 修饰符。我们可以仿照 Modifier.offset{ } 的方式来自定义,但我们不需要自己写padding的逻辑,因为我们可以直接查看系统内置padding源码的实现,找到下面的代码:

// Padding.kt
@Stable
fun Modifier.padding(paddingValues: PaddingValues) =this.then(PaddingValuesModifier(paddingValues = paddingValues,inspectorInfo = debugInspectorInfo {name = "padding"properties["paddingValues"] = paddingValues}))
private class PaddingValuesModifier(val paddingValues: PaddingValues,inspectorInfo: InspectorInfo.() -> Unit
) : LayoutModifier, InspectorValueInfo(inspectorInfo) {override fun MeasureScope.measure(measurable: Measurable,constraints: Constraints): MeasureResult {require(paddingValues.calculateLeftPadding(layoutDirection) >= 0.dp &&paddingValues.calculateTopPadding() >= 0.dp &&paddingValues.calculateRightPadding(layoutDirection) >= 0.dp &&paddingValues.calculateBottomPadding() >= 0.dp) {"Padding must be non-negative"}val horizontal = paddingValues.calculateLeftPadding(layoutDirection).roundToPx() +paddingValues.calculateRightPadding(layoutDirection).roundToPx()val vertical = paddingValues.calculateTopPadding().roundToPx() +paddingValues.calculateBottomPadding().roundToPx()val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))val width = constraints.constrainWidth(placeable.width + horizontal)val height = constraints.constrainHeight(placeable.height + vertical)return layout(width, height) {placeable.place(paddingValues.calculateLeftPadding(layoutDirection).roundToPx(),paddingValues.calculateTopPadding().roundToPx())}}override fun hashCode() = paddingValues.hashCode()override fun equals(other: Any?): Boolean {val otherModifier = other as? PaddingValuesModifier ?: return falsereturn paddingValues == otherModifier.paddingValues}
}    

我们直接把上面的源码复制一份,然后把不必要的调试信息去掉,稍微改一下,我们自己的 lambda 版本的padding{ } 修饰符就诞生了:

@Stable
private fun Modifier.padding(paddings: Density.() -> PaddingValues) = then(PaddingValuesModifier(paddings, rtlAware = true)
)private class PaddingValuesModifier(val paddings: Density.() -> PaddingValues,val rtlAware: Boolean,
) : LayoutModifier {override fun MeasureScope.measure(measurable: Measurable,constraints: Constraints): MeasureResult {val paddingValues = paddings()require(paddingValues.calculateLeftPadding(layoutDirection) >= 0.dp &&paddingValues.calculateTopPadding() >= 0.dp &&paddingValues.calculateRightPadding(layoutDirection) >= 0.dp &&paddingValues.calculateBottomPadding() >= 0.dp) {"Padding must be non-negative"}val horizontal = paddingValues.calculateLeftPadding(layoutDirection).roundToPx() +paddingValues.calculateRightPadding(layoutDirection).roundToPx()val vertical = paddingValues.calculateTopPadding().roundToPx() +paddingValues.calculateBottomPadding().roundToPx()val placeable = measurable.measure(constraints.offset(-horizontal, -vertical))val width = constraints.constrainWidth(placeable.width + horizontal)val height = constraints.constrainHeight(placeable.height + vertical)return layout(width, height) {if (rtlAware) {placeable.placeRelative(paddingValues.calculateLeftPadding(layoutDirection).roundToPx(),paddingValues.calculateTopPadding().roundToPx())} else {placeable.place(paddingValues.calculateLeftPadding(layoutDirection).roundToPx(),paddingValues.calculateTopPadding().roundToPx())}}}override fun hashCode() : Int {var result = paddings.hashCode()result = 31 * result + rtlAware.hashCode()return result}override fun equals(other: Any?): Boolean {if (this === other) return trueval otherModifier = other as? PaddingValuesModifier ?: return falsereturn paddings == otherModifier.paddings &&rtlAware == otherModifier.rtlAware}
}

现在,我们把前面例子中的使用函数类型传递dp值的最内层的padding应用改一下,改成使用我们自己的 lambda 版本:

// ...省略其余代码
@Composable
private fun PaddingInnerDeferred(padding: () -> Dp) { Text(text = "PaddingInnerDeferred",modifier = Modifier//.padding(start = padding()).padding {PaddingValues(start = padding())}.fillMaxWidth().background(getRandomColor()))
}

再次运行代码,查看效果:

在这里插入图片描述

可以看到,上面的 Inner Text 上读取变化的padding 值时,也不会闪了,腰也不疼了,腿也不酸了,再也不会发生重组了。真香😃

避免重组循环(循环阶段依赖项)

系统始终按照相同的顺序来调用 Compose 的各个阶段,并且无法在同一帧中后退。不过,这并未禁止应用跨不同的帧进入组合循环。请思考以下示例:

Box {var imageHeightPx by remember { mutableStateOf(0) }Image(painter = painterResource(R.drawable.rectangle),contentDescription = "I'm above the text",modifier = Modifier.fillMaxWidth().onSizeChanged { size ->// Don't do thisimageHeightPx = size.height})Text(text = "I'm below the image",modifier = Modifier.padding(top = with(LocalDensity.current) { imageHeightPx.toDp() }))
}

在本例中,我们实现了一个垂直列(并不理想),其顶部是图片,而图片下方则是文本。我们要使用 Modifier.onSizeChanged() 来获取图片的解析大小,然后对文本使用 Modifier.padding() 以将其下移。从 Px 转换回 Dp 的过程很不自然,这就说明代码存在一些问题。

该示例的问题在于,我们没有在单个帧中达到“最终”布局。该代码依赖发生多个帧,它会执行不必要的工作,并导致界面在用户屏幕上跳动。

接下来,我们要逐一检查每个帧,看看发生了什么:

在第一帧的组合阶段,imageHeightPx 的值为 0,并且文本是按照 Modifier.padding(top = 0) 提供的。接着,布局阶段紧随其后,并且系统调用 onSizeChanged 修饰符的回调。此时,imageHeightPx 更新为了图片的实际高度。Compose 为下一帧安排重组。在绘制阶段,由于值发生的更改尚未得到反映,因此,系统渲染文本时将内边距设为了 0

然后,Compose 启动根据 imageHeightPx 的值发生的更改安排的第二帧。系统在 Box 内容块中读取状态,并在组合阶段调用该状态。此时,系统提供文本时会采用与图片高度相匹配的内边距。在布局阶段,代码确实会再次设置 imageHeightPx 的值,但不会安排重组,因为该值会保持不变。

最终,我们会在文本上实现所需的内边距,但这并不是最理想的情况,因为我们还要使用一个额外的帧将内边距值传递回其他阶段,而这会导致产生一个内容重叠的帧。
在这里插入图片描述
该示例可能显得有些刻意,但请注意以下通用模式:

  • Modifier.onSizeChanged()、onGloballyPositioned() 或一些其他布局操作
  • 更新某种状态
  • 使用该状态作为对布局修饰符(padding()、height() 或类似元素)的输入
  • 可能会重复

若要修复以上示例,您可以使用适当的布局,以上示例可以使用一个简单的 Column() 来实现,但您可能会遇到需要进行自定义的更复杂的示例,这时需要编写自定义布局。

在列表中使用 key 指定唯一 id

Compose编译器会在每个Composable函数的函数体中插入一个group,每个 group都会使用一个 key 作为唯一标识,这个 key 是使用Composable在源码中的位置信息来生成的(由Compose runtime生成)。也就是说这个 key 就是Composable的标识。 一般情况下Composable在源码中的位置是不会变的,但是使用列表的情况除外。当代码中会生成一个Composable列表时,对Compose runtime来说,分配唯一标识是很困难的。

例如下面的例子:

在这里插入图片描述

在这种情况下,每次都从相同的位置调用Talk(talk),但是每个Talk表示列表上的不同项,因此是树上的不同节点。在这种情况下,Compose runtime依赖于调用的顺序来生成唯一的id,并且仍然能够区分它们。

当将一个新元素添加到列表的末尾时,这段代码仍然可以正常工作,因为其余的调用保持在与以前相同的位置。但如果我们在顶部或中间的某个位置添加元素呢?Compose runtime将重新组合该位置以下的所有Talk,因为它们改变了位置,即使它们的输入没有变化。这是非常低效的(尤其是对于长列表而言),因为这些调用本应该被跳过。

为了解决这个问题,Compose提供了一个用来设置 key 的 Composable,因此我们可以手动为Composable调用分配一个显式的 key

在这里插入图片描述
在这个例子中,我们使用talk.id(可能是唯一的)作为每个Talkkey,这将允许runtime保存列表中所有项的标识,而不管它们的位置如何。

记住,位置记忆 是 Composable 函数的属性之一,这一部分在 Jetpack Compose 深入探索系列一:Composable 函数 中有介绍过,感兴趣的话可以去看看。

derivedStateOf

derivedStateOf 主要作用是将其他 state 转换成另一个 state,其在性能优化方面的应用场景和示例,之前在 Jetpack Compose中的副作用 中的 derivedStateOf { } VS remember(key) { } 部分有提到过,感兴趣的可以点击过去查看。这里不重复列举了。

总而言之:当你需要依赖其他状态生成另一个状态来更新 UI,并且其他状态的更新频率远远高于 UI 的刷新频率时,这时就是可以考虑使用 derivedStateOf 的时候了。

在这里插入图片描述

@Stable 和 @Immutable

这两个注解都是关于稳定性的话题,在前面的例子中有提到过,但是没有详细展开。下面再来看一下:

@Composable
fun Sample() {Column(modifier = Modifier.padding(4.dp).shadow(1.dp, shape = CutCornerShape(topEnd = 8.dp)).background(getRandomColor()).padding(4.dp),horizontalAlignment = Alignment.CenterHorizontally) {var counter by remember { mutableStateOf(0) }val myData = remember { MyData(0) }Text("Text1 Read counter: $counter", color = Color.White, modifier = Modifier.background(getRandomColor()))Button(modifier = Modifier.fillMaxWidth(),colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),onClick = {counter++myData.value = myData.value + 1},shape = RoundedCornerShape(5.dp)) {MyText(myData.value)}}
}class MyData(var value: Int = 0)@Composable
fun MyText(counter: Int = 0) {Column {Text("MyText: Read myData.value: $counter", color = Color.White, modifier = Modifier.background(getRandomColor()))Text("Another Text", color = Color.White, modifier = Modifier.background(getRandomColor()))}
}

在这里插入图片描述

这里的 MyData 类的myData对象并没有被 mutableStateOf() 包装,按理说它应该是一个不能被Composable观察到的状态,但是这里当Button组件重组时,读取myData对象值的MyText组件也发生重组了。这说明Compose还是为它执行了Composable函数。

这是因为myData对象虽然是被定义为 val 不可被重新赋值,但是 MyData 类中包含了可变成员var value,这就意味着myData对象并不是真正意义上的不可变对象,因为随时都可以通过其可变成员修改它的数据,实际上面代码中也是这么干的,在点击事件中,不断的修改它的value成员值+1。因此它被编译器推断为是一个不稳定的类,而不稳定的类会破坏智能重组,因此意味着不能跳过重组。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

智能重组指的是当Composable函数的输入没有改变且这些输入被认为是稳定时,跳过重新组合。稳定性在这个意义上是一个非常重要的概念,因为它意味着Compose runtime可以安全地读取和比较这些输入,以在需要时跳过重新组合。稳定性的最终目标是帮助runtime。

那么什么样的类会被编译器确认为稳定的类型呢?总的来说,一个稳定的类型需要满足如下三点:

  • 对相同的两个实例调用 equals 的结果始终相同。这意味着比较是一致的,因此运行时可以依赖它。
  • 当类型的公开属性更改时,总是会通知Composition组合。否则,我们可能会遇到输入与最新状态不同步的情况。为了确保不会发生这种情况,总是针对此类情况触发重新组合。智能重组合无法依赖此输入。
  • 所有公开属性也必须是稳定的。

默认情况下,所有基本类型都是稳定的String类型和所有函数类型也是稳定的

有些类型不是不可变的,但可以被Compose假设为稳定的,它们可以用@Stable 进行注解。 对于我们在代码中创建的自定义类型,我们可以判断它们是否符合上面列出的属性,并使用@Immutable@Stable注解手动将它们标记为稳定类型@Immutable@Stable 都是通过 @StableMarker 元注解注解的注解,如果要使用这三个注解来标注一个类为稳定的,则该类必须也满足上面三点要求。

因此对于上面的示例代码,即便将 MyData 类上手动添加 @Stable 注解也是不能避免重组的,因为其公开属性就是不稳定的,这违背了上面的要求。另外这三个注解都是开发者对编译器的承诺,但编译器在编译时不会验证它们。因此依靠开发者履行契约是相当危险的,而且很难长期维持,所以编译器会自动推断类的稳定性

为了推断一个类的稳定性,Compose考虑了不同的东西。当类的所有字段都是只读且稳定时,类型被推断为稳定。像 class Fooclass Foo(val value: Int) 这样的类将被推断为稳定的,因为它们没有字段或只有稳定字段。然后像 class Foo(var value: Int) 这种将被推断为不稳定的。对于由其他类组成的类,如 class Foo(val bar: Bar, val bazz: Bazz),稳定性被推断为所有参数稳定性的组合。这种情况会通过递归来解决。

内部可变状态这样的东西也会使类不稳定。其中一个例子如下:
在这里插入图片描述
这种状态会随着时间的推移发生变化,即使它是由类本身在内部发生变化。这意味着运行时并不能真正的相信它会总是保持一致不变的。

总的来说,Compose编译器只有在能够证明一个类型是稳定的时候才会考虑它。例如:一个接口被认为是不稳定的,因为Compose不知道它将如何实现

再来看一个示例代码:
在这里插入图片描述
在这个例子中,我们得到一个List作为参数,它能以可变的方式实现(它可以是List也可以是MutableList实现)。对编译器来说,这种情况下推断会变得及其困难,所以它会直接假设它是不稳定的。

对于开头的例子,我们只需将 MyData 类中的公开属性value改为 val 类型即可使编译器推断它为稳定类型(因为它只有一个公开可变类型而且是基本类型):

@Composable
fun Sample() {Column(modifier = Modifier.padding(4.dp).shadow(1.dp, shape = CutCornerShape(topEnd = 8.dp)).background(getRandomColor()).padding(4.dp),horizontalAlignment = Alignment.CenterHorizontally) {var counter by remember { mutableStateOf(0) }var myData = remember { MyData1(0) }Text("Text1 Read counter: $counter", color = Color.White, modifier = Modifier.background(getRandomColor()))Button(modifier = Modifier.fillMaxWidth(),colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),onClick = {counter++myData = MyData1(myData.value + 1)},shape = RoundedCornerShape(5.dp)) {MyText(myData.value)}}
}class MyData1(val value: Int = 0)@Composable
fun MyText(counter: Int = 0) {Column {Text("MyText: Read myData.value: $counter", color = Color.White, modifier = Modifier.background(getRandomColor()))Text("Another Text", color = Color.White, modifier = Modifier.background(getRandomColor()))}
}

在这里插入图片描述

这样MyText组件便不会发生重组了,但同时它也不会读取到 myData中的值。

关于 @Satble 如何使用。官方给出了一个简单的示例:

// 告诉编译器,其类型是稳定的可以跳过不必要的重组
@Stable
interface UiState<T> {val value T?val exception: Throwable?val hasError: Booleanget() = exception != null
}

在目前来讲,Compose中使用 @Immutable@Satble 的效果是一样的实际效果是一样的。使用它们都会开启和优化智能重组。但实际中应该谨慎使用它们,而是让编译器自动推断类的稳定性。

建议:Composable中使用的所有class中的var修饰的公开属性都使用 mutableStateOf() 来修饰。(即满足 “所有公开属性的更改时,总是会通知Composition组合” 的要求)

除了使用注解,保证稳定性的另一种方法是使用不可变的集合类,例如使用 kotlinx 提供的 immutable 集合工具类,不过这需要单独添加依赖:

implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:$version")

在这里插入图片描述

如果不满足使用前面注解的条件,也不能使用可变的集合类,那么还有一种笨方法,就是将接受不稳定的数据类型手动改为稳定类型。例如下面的代码中,组件接受了一个不稳定的类型:

@Composable
fun WelcomeView(user: UnStableUser) {Text(text = "Welcome ${user.username}!")
}
data class UnStableUser(var id: String,var email: String,var username: String
)

那么可以自己手工写一个工具方法将此类转换成另一个 Stable 的类:

data class User(val id: String,val email: String,val username: String
)fun UnStableUser.toUser(): User {return User(id = id,email = email,username = username)
}fun User.toUnStableUser(): ExternalUser {return UnStableUser(id = id,email = email,username = username)
}

在转换的时候,可以尽量只选择那些被UI组件使用到的字段,避免引入多余的可变因素。例如上面的使用user的组件其实只使用到了一个 username,在这种情况下其实将输入参数改成String也是可以的。但如果参数较多,还是通过转换类比较方便。

优化 lambda 回调

前面提到 lambda 版本的修饰符可以在读取可变状态时提升性能,但是某些情况下,使用 lambda 也会带来一些性能问题。比如我们通常使用的 lambda 回调:

@Composable
fun SomeLayout(onClick: () -> Unit) {Button(onClick = onClick) {Text(text = "text")}
}

假如我们在另一个 Composable 中调用它

@Composable
fun SomeComposable() {SomeLayout {// ...}
}

在 Composable 中,这种 lambda 回调经常被用于控制反转,即将事件处理的主动权交于外部调用者来实现,但是在 kotlin 中,这种方式的 lambda 调用会生成一个匿名内部类对象(对应JVM平台):

object: Function0<Unit> {override fun invoke() {// ...}
}

由于 Java 中没有所谓的函数类型,当函数类型被翻译到 JVM 平台时,就会以对应的函数接口类型来替代不同参数个数的kotlin函数类型Function0...Function22,最多有22个)。而这也是问题所在,调用 lambda 的 Composable 实际上无意间向子组件中传递了一个匿名内部类对象。前面提到过,Compose编译器会对 Composable 中使用的数据类型进行自动类型推断,如果被推断为不稳定的类型,那么就不会执行智能重组(不能跳过重组)。因此,当调用者无意间在子组件暴露的 lambda 中引入了可变因素,那么该匿名内部类对象很可能就会被Compose编译器推断为不稳定,从而导致每次都会触发不必要的重组。

来看下面的例子,

@Composable
fun MainScreen(viewModel: MainViewModel = viewModel()) { Box(modifier = Modifier.fillMaxSize(),contentAlignment = Alignment.Center) {CoffeeSelector(type = viewModel.type,onTypeChange = {viewModel.changeType(it)} )}
}@Composable
fun CoffeeSelector(type: Type,onTypeChange: (Type) -> Unit,modifier: Modifier = Modifier
) {Column(modifier = modifier.padding(10.dp),horizontalAlignment = Alignment.CenterHorizontally) {Box(modifier = Modifier.size(100.dp).background(getRandomColor()),contentAlignment = Alignment.Center) {val text = when(type) {Type.BIG -> "大杯"Type.MIDDLE -> "中杯"Type.SMALL -> "小杯"}Text(text = text, fontSize = 16.sp, color = Color.White)}Spacer(modifier = Modifier.height(16.dp))Column {MyButton("大杯") {onTypeChange(Type.BIG)}MyButton("中杯") {onTypeChange(Type.MIDDLE)}MyButton("小杯") {onTypeChange(Type.SMALL)}}}
}@Composable
fun MyButton(text: String, onClick: () -> Unit) {Button(onClick = onClick,colors = ButtonDefaults.buttonColors(backgroundColor = getRandomColor()),modifier = Modifier.fillMaxWidth()) {Text(text = text,fontSize = 16.sp,color = Color.White,modifier = Modifier.fillMaxWidth().wrapContentWidth(Alignment.CenterHorizontally))}
}enum class Type { BIG, MIDDLE, SMALL }
class MainViewModel: ViewModel() {var type by mutableStateOf(Type.MIDDLE)private setfun changeType(type: Type) {this.type = type}
}
class MainActivity : ComponentActivity() { private val viewModel: MainViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState) setContent {MainScreen(viewModel)}}
}

在这里插入图片描述

上面代码中,三个按钮将选择结果通过点击事件回调,回传给调用者,调用者获得选择结果后,更新状态值,这会再次将状态值反馈给组件显示结果。显示选择结果的文本组件会发生重组,这是正常的,但是底部的三个按钮它们没有读取任何状态,每次点击的时候却也发生了重组。这就是问题所在。正常情况下,应该只有展示选择结果的文本组件会发生重组,底部的三个按钮不应该发生任何重组。

这是由于调用 CoffeeSelector 的地方通过 lambda 的方式创建了匿名内部类对象(onTypeChange的调用),而在这个 lambda 中引入了 viewModel 这个局部变量,因此匿名内部类对象会持有对该局部变量的引用,而我们观察到 MainViewModel 其实是一个不稳定的类型。(因为它具有公开可变类型)

所以CoffeeSelector 实际上是读取了一个包含不稳定的viewModel对象的匿名内部类对象,该对象被编译器推断为不稳定,CoffeeSelector中的三个按钮实际上都读取了该匿名内部类对象,因此每次它们都会发生重组。

那么该如何避免这种多余的重组发生呢?首先我们可以直接在调用处避免直接使用 lambda 的形式调用回调,而是改用函数引用进行传递:

@Composable
fun MainScreen(viewModel: MainViewModel = viewModel()) { Box(modifier = Modifier.fillMaxSize(),contentAlignment = Alignment.Center) {CoffeeSelector(type = viewModel.type,onTypeChange = viewModel::changeType)}
}

现在运行之后就会发现,点击按钮时,三个按钮不会再发生重组了,而只要上面展示结果的文本组件才会重组:

在这里插入图片描述

假如 lambda 回调的参数和要调用的 viewModel 的方法参数不匹配,比如个数不一样,那么就不能直接引用 viewModel 的方法,此时可以采用另一种办法,通过 remember 来创建一个 函数类型的 lambda 变量,同样可以达到目的,虽然它的可读性不是很好:

@Composable
fun MainScreen(viewModel: MainViewModel = viewModel()) { val changeTypeLambda = remember<(Type) -> Unit> {{viewModel.changeType(it)}}Box(modifier = Modifier.fillMaxSize(),contentAlignment = Alignment.Center) {CoffeeSelector(type = viewModel.type,onTypeChange = changeTypeLambda)}
}

最后还有一种迂回战术,即不在 lambda 中引入不稳定的类型,或者说引入被Compose认可的类型,例如 mutableStateOf() :

@Composable
fun MainScreen(viewModel: MainViewModel = viewModel()) { var type by remember { mutableStateOf(Type.MIDDLE) }Box(modifier = Modifier.fillMaxSize(),contentAlignment = Alignment.Center) {CoffeeSelector(type = type, onTypeChange = {type = it})}
}

这里 type 的类型是枚举类型,它是稳定的类型(因为它不可修改),虽然 type 本身是 var 可变的,但是同时 type 是通过 mutableStateOf() 创建,这意味着,一旦它发生变化,它就会通知组合。因此这种方式是官方的标准用法,也是我们日常开发最常见用法。这种方式也能避免上面例子中的三个按钮的无必要的重组。

测试和开启性能报告

关于 Jetpack Compose 的性能测试,可以参考官方的使用 Macrobenchmark 检查应用性能的Codelab,其中列出了使用 Macrobenchmark 库对应用进行性能测试的详细步骤。这里有一点需要注意的是 benchmark 测试应当是基于真实设备且运行 release 版本的测试(但是你可以使用debug签名),以达到更加接近实际的测试结果。

运行 benchmark 测试后,可以通过 logcat 观察两个重要的指标:TTID (timeToInitialDisplay) 和 TTFD (timeToFullDisplay ),分别表示第一帧准备好和内容完全绘制完所需的时间。

在这里插入图片描述
这里表示启动时间的最小值为 294.8 毫秒,中位数为 301.5 毫秒,最大值为 314.8 毫秒。

TTID 指标跟应用的启动类型密切相关,在Android中,应用启动类型主要分为三种:热启动、温启动和冷启动。它们分别对应的生命周期方法如下图所示:

在这里插入图片描述

在冷启动开始时,系统有三个任务,分别是:

  1. 加载并启动应用。
  2. 在启动后立即显示应用的空白启动窗口。
  3. 创建应用进程。

系统一旦创建了应用进程,应用进程就负责后续阶段:

  1. 创建 Application 对象。
  2. 启动主线程消息循环。
  3. 创建主 Activity。
  4. 填充视图内容。
  5. 进行View布局。
  6. 执行初始绘制。

一旦应用进程完成第一次绘制,系统进程就会换掉当前显示的后台窗口,替换为主 activity。此时,用户可以开始使用应用。对于启动性能而言,最容易出现问题的阶段是在创建 Application 和创建主 Activity的过程中。

在这里插入图片描述

TTID 发生在 Activity#onStart 阶段,在应用首次冷启动时,它就是从 Application#onCreate 到第一帧开始绘制时的耗时,而在热启动的情况下,它的耗时比冷启动要少很多。注意 TTFD 是内容完全绘制完毕的时间,此时用户真正的可以与应用进行交互操作,注意它跟TTID的区别。

在这里插入图片描述

通过 Macrobenchmark 可以自动衡量应用呈现第一帧所需的时间TTID 。不过,直到第一帧呈现之后,应用内容仍未完成加载的情况也很常见,而您可能想要了解用户需要等待多长时间才能使用应用。此等待时间称为完全显示所用时间 TTFD,即应用内容已全部加载,且用户可以与应用互动所需的时间。你可以使用 Activity.reportFullyDrawn() 函数来主动报告应用真正完全显示的时间,以便向 Macrobenchmark 库传达更精准的时间。

如果是在 Composable 中,可以调用 ReportDrawnWhenReportDrawnAfter来报告,但这两个 Api 是从 activity-compose 1.7 开始支持的,需要添加对应的依赖。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.luyixian.cn/news_show_78320.aspx

如若内容造成侵权/违法违规/事实不符,请联系dt猫网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

路由协议(OSPF、ISIS、BGP)实验配置

目录 OSPF基础实验 建立OSPF邻居 配置虚连接 配置接口的网络类型 配置特殊区域 配置路由选路 配置路由过滤 ISIS基础实验配置 配置ISIS邻居建立 配置认证 配置路由扩散 配置路由过滤 配置定时器 BGP基础实验配置 建立BGP对等体 建立IBGP对等体 建立EBGP对等体…

音频基础知识简述 esp-sr 上手指南

此篇博客先对音频基础知识进行简要叙述&#xff0c;然后帮助读者入门 esp-sr SDK。 1 音频的基本概念 1.1 声音的本质 声音的本质是波在介质中的传播现象&#xff0c;声波的本质是一种波&#xff0c;是一种物理量。 两者不一样&#xff0c;声音是一种抽象的&#xff0c;是声…

第二章Linux操作语法1

文章目录vi和vim常用的三种模式vi和vim快捷键Linux开机&#xff0c;重启用户管理用户信息查询管理who和whoami用户组信息查询管理用户和组的相关文件实用指令集合运行级别帮助指令manhelp文件管理类pwd命令ls命令cd命令mkdir命令rmdir命令rm命令touch命令cp指令mv指令文件查看类…

10.单点登录原理及JWT实现

单点登录原理及JWT实现 一、单点登录效果 首先我们看通过一个具体的案例来加深对单点登录的理解。案例地址&#xff1a;https://gitee.com/xuxueli0323/xxl-sso?_fromgitee_search 把案例代码直接导入到IDEA中 然后分别修改下server和samples中的配置信息 在host文件中配置 …

【Opencv项目实战】图像的像素值反转

文章目录一、项目思路二、算法详解2.1、获取图像信息2.2、新建模板2.3、图像通道顺序三、项目实战&#xff1a;彩图的像素值反转&#xff08;方法一&#xff09;四、项目实战&#xff1a;彩图的像素值反转&#xff08;方法二&#xff09;五、项目实战&#xff1a;彩图转换为灰图…

Spark Catalyst

Spark Catalyst逻辑计划逻辑计划解析逻辑计划优化Catalyst 规则优化过程物理计划Spark PlanJoinSelection生成 Physical PlanEnsureRequirementsSpark SQL 端到端的优化流程&#xff1a; Catalyst 优化器 : 包含逻辑优化/物理优化Tungsten : Spark SQL的优化过程 : 逻辑计划 …

pytorch安装的超级详细教程(没有之一)

一、发展历程 &#xff08;简单介绍&#xff09; (15年)caffe --> (16年)tensorflow1.x --> (17年)keras --> (18年)Tensorflow2.x --> (19年)pytorch。 面向gihub开源项目编程。 向下支持比较好&#xff0c;各个版本之间支持比较好&#xff0c;兼容性强。 版本…

自动驾驶介绍系列 ———— 看门狗

文章目录硬件看门狗软件看门狗差异分析延申窗口看门狗硬件看门狗 硬件看门狗的本质上是一个定时器电路。通常存在一个输入&#xff0c;输入到MCU的RST端。在正常工作状态下&#xff0c;MCU每隔固定时间间隔会输出一个信号给RST端&#xff0c;实现对看门狗端清零。如果在指定的时…

全网最全之接口测试【加密解密攻防完整版】实战教程详解

看视频讲的更详细&#xff1a;https://www.bilibili.com/video/BV1zr4y1E7V5/? 一、对称加密 对称加密算法是共享密钥加密算法&#xff0c;在加密解密过程中&#xff0c;使用的密钥只有一个。发送和接收双方事先都知道加密的密钥&#xff0c;均使用这个密钥对数据进行加密和解…

JAVA开发运维(nginx工作原理)

nginx源码目录结构&#xff1a; . ├── auto 自动检测系统环境以及编译相关的脚本 │ ├── cc 关于编译器相关的编译选项的检测脚本 │ ├── lib nginx编译所需要的一些库的检测脚本 │ ├── os 与平台相关的一些系统参数…

【3.6】链表、操作系统CPU是如何执行程序的、Redis数据类型及其应用

链表 题目题型203. 移除链表元素 - 力扣&#xff08;LeetCode&#xff09;辅助头节点解决移出head问题707. 设计链表 - 力扣&#xff08;LeetCode&#xff09;辅助头节点206. 反转链表 - 力扣&#xff08;LeetCode&#xff09;迭代 / 递归19. 删除链表的倒数第 N 个结点 - 力扣…

web餐饮开源程序

简介 一款专门针对餐饮行业而开发桌面应用程序 技术 借助Panuon.UI.Silver控件库&#xff0c;开发的一款餐饮软件。 运行环境&#xff1a;.NETFramework,Versionv4.8。 运行数据库&#xff1a;MySql。 ORM框架&#xff1a;SqlSugar。 第三方插件&#xff1a;Panuon.UI.Silv…

网上订餐管理系统的设计与实现

技术&#xff1a;Java、JSP等摘要&#xff1a;随着信息技术的广泛使用&#xff0c;电子商务对于提高管理和服务水平发挥着关键的作用。越来越多的商家开始着手于电子商务建设。电子商务的发展为人们的生活提供了极大的便利&#xff0c;也成为现实社会到网络社会的真实体现。当今…

来吧!接受Kotlin 协程--线程池的7个灵魂拷问

前言 之前有分析过协程里的线程池的原理&#xff1a;Kotlin 协程之线程池探索之旅(与Java线程池PK)&#xff0c;当时偏重于整体原理&#xff0c;对于细节之处并没有过多的着墨&#xff0c;后来在实际的使用过程中遇到了些问题&#xff0c;也引发了一些思考&#xff0c;故记录之…

网络协议丨从物理层到MAC层

我们都知道TCP/IP协议其中一层&#xff0c;就是物理层。物理层其实很好理解&#xff0c;就是物理攻击的物理。我们使用电脑上网时的端口、网线这些都属于物理层&#xff0c;没有端口没有路由你没有办法上网。网线的头我们叫水晶头&#xff0c;也是物理层的一份子。如果你的面前…

深入理解java虚拟机精华总结:性能监控和故障处理工具、类加载机制

深入理解java虚拟机精华总结&#xff1a;性能监控和故障处理工具、类加载机制性能监控和故障处理工具、类加载机制jpsjstatjinfojmapjhatjstackVisualVM类加载机制类加载的时机类加载的过程加载验证准备解析初始化类加载器类与类加载器双亲委派模型破坏双亲委派模型往期内容&am…

X264简介-Android使用(二)

X264简介-Android使用&#xff08;二&#xff09; 4、Ubuntu上安装ffmpeg&#xff1a; 检查更新本地软件包&#xff08;如果未更新&#xff0c;reboot Vmware&#xff09;&#xff1a; sudo apt update sudo apt upgrade官网下载的source文件安装&#xff1a; http://ffmpe…

做数据分析有前景吗?

当然有前景的。 每个行业都有发展前景&#xff0c;只是看你自身的技能情况或者关系人脉、软实力方面是否到位&#xff0c;不同的行业要求不一样。作为数据分析领域而言&#xff0c;属于IT行业&#xff0c;看的是你的专业技能&#xff1b;只要你技能过硬&#xff0c;就能在行业…

面向对象设计模式:行为型模式之状态模式

文章目录一、引入二、状态模式2.1 Intent 意图2.2 Applicability 适用性2.3 类图2.4 状态模式与策略模式的对比2.5 状态模式实例&#xff1a;糖果机2.6 状态模式实例&#xff1a;骑自行车升降档一、引入 State Diagram 状态图&#xff1a; A state diagram is a type of diagr…

OceanBase 第六期技术征文活动|小鱼还能“更快”吗?你来试试

2022 年 8 月 10 日&#xff0c;我们在 OceanBase 年度发布会上正式发布了 OceanBase 4.0&#xff08;代号&#xff1a;小鱼&#xff09;&#xff0c;并在现场展区尝试做了一些有趣的事情&#xff0c;“小鱼”可以单机版部署在个人 PC 以及树莓派&#xff0c;让来到现场的开发者…