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

本文详细介绍了Jetpack Compose中重组作用域的概念及其影响,强调只有读取可变状态的作用域才会被重组。讨论了内联组件的重组作用域、隔离重组作用域的方法,以及如何避免不必要的重组。文章还提到了优化Modifier的状态读取,使用derivedStateOf降低重组次数,以及如何处理lambda回调和log日志带来的性能问题。最后,作者提醒读者关注Compose Compiler的编译报告,以便更好地理解和优化性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

这句话的意思是只有读取 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 &#
参考资源链接:[精通Android 13开发:实战指南](https://wenku.youkuaiyun.com/doc/1h9tge5w6v?utm_source=wenku_answer2doc_content) 为了在使用Jetpack Compose构建应用时优化性能并处理资源耗尽问题,首先需要理解Jetpack Compose的工作原理Kotlin语言的高级特性。本书《精通Android 13开发:实战指南》为开发者提供了一套实用的解决方案最佳实践,以应对性能挑战。 首先,Jetpack Compose是一个声明式的UI框架,它能够更高效地处理UI渲染。为了优化性能,开发者应遵循以下策略: 1. **避免不必要的重组**:重组是当State或可观察对象发生变化时,Compose重新评估执行Composable函数的过程。为了避免性能下降,应该最小化State的使用,并在适当的时候使用remember或mutableStateOf来缓存值。 2. **使用Lazy系列组件**:对于列表网格布局,使用LazyColumn、LazyRow等组件可以有效减少不必要的UI元素的创建重组,从而提高性能。 3. **利用rememberSaveable**:在需要保存UI状态时,使用rememberSaveable而不是remember,因为rememberSaveable可以自动处理可保存的状态,如Intent、Bundle等。 4. **优化高阶函数使用**:在高阶函数中传递数据时,应确保数据不被重复创建,特别是在频繁调用的回调中。 5. **使用协程进行异步处理**:Kotlin的协程框架提供了强大的异步处理能力。在操作耗时任务时,应当利用协程挂起函数协程作用域来避免阻塞主线程。 6. **监控分析性能**:使用Android Profiler工具来监控应用的CPU、内存网络使用情况。这对于识别性能瓶颈资源泄漏非常有帮助。 通过将这些性能优化策略结合到应用中,开发者可以有效地避免资源耗尽的问题。建议深入阅读《精通Android 13开发:实战指南》中的相关章节,了解详细的应用案例解决方案。此外,通过实践测试,开发者能够更好地掌握如何使用Jetpack ComposeKotlin来构建高效、稳定的应用程序。 参考资源链接:[精通Android 13开发:实战指南](https://wenku.youkuaiyun.com/doc/1h9tge5w6v?utm_source=wenku_answer2doc_content)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

川峰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值