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

Posted 川峰

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Jetpack Compose 中的重组作用域和性能优化相关的知识,希望对你有一定的参考价值。

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

这句话的意思是只有读取 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 的组件,例如 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: Jetpack Compose Effect 的作用

Jetpack Compose Effect 的作用

Jetpack Compose Effect 的作用

Jetpack Compose – LazyColumn 不重组

Recompose 方法在 Jetpack Compose 中不起作用

Jetpack Compose:更新列表元素内容时不会发生重组