Compose的重组作用域

什么是重组

在了解Compose的重组作用域之前,我们先来了解下重组是什么
对于Jetpack Compose,即使是初学者也都知道它被称为声明式UI开发,相对于之前通过xml方式的命令式UI。而在命令式UI模式下,如果我们想要更新一个widget,我们需要拿到这个widget通过调用其setter函数来改变其内部状态。但是Jetpack Compose中的widget相对无状态并不提供setter/getter函数,而是重新调用可组合函数来实现更新,这个过程就叫重组。目前Jetpack Compose重组是在UI线程执行的,所以重组过程是有性能消耗的。为此,Compose编译器内部做了大量工作保证重组的范围尽可能小来提升重组效率。

重组范围

重组范围在Compose中是一个很重要的组成部份,通过尽量缩小重组范围来减少Compose在准备每一帧时的工作量。

  • 它们是通过重组更新Compose节点的最小单位
  • 它们通过跟踪观察基于快照的状态对象,并在这些状态变化时会无效 对于每个non-inline并且返回Unit的可组合函数, Compose编译器会生成代码将这个函数包装到一个可重组块中,当这个代码块被标记为Invalidated时,Compose Runtime将确保在渲染下一帧之前该代码块被重新执行(重组)。
    PS:

    是怎样被标记为Invalidated,这块关系到State/Snapshot暂时不展开,反正先记住,不依赖State的代码是不参与重组的。

为什么要non-inline且没有返回值的函数才可作为可重组块

inline函数,在编译过程中会直接将函数体拷贝到调用处,所以它只能共享调用者的重组范围
对于有返回值的函数,由于返回值的变化会影响调用者,因此无法单独重组,而必须连同调用方一同参与重组,因此它不能作为入口被标记为invalid。假设如果只是单独发生重组,其返回值改变了,但是其调用者并没有重组,还是使用的旧值,这不是预期的结果。

表达式和函数调用

在深入了解最小重组范围前,我们先了解一个关于常规旧函数调用如何工作的基本概念。在Java/Kotlin中,当你将表达式传递给函数时,该表达式会在调用函数中计算。

println("hello" + "world")

它等价于

val arg = "hello" + "world"
println(arg)

简而言之,先执行表达式得到结果,再执行函数调用(了解这个,对于确认重组作用域有一定的帮助)

确认重组作用域

大致了解了表达式和函数调用后,我们通过一个简单的例子过一下如何确认重组作用域。先看下面的代码块

@Composable fun Foo() {
    var text by remember { mutableStateOf("") }
    println("TAG: Foo Scope")
    Button(onClick = { text = "$text\n$text" }) {
        println("TAG: Button lambda Scope")
        Text(text).also {
            println("TAG: Text Scope")
        }
    }
}

1. 为什么text改变后,不是只有Text发生重组

点击Button时,State text 被重新赋值而改变,而State text只有Text访问到,为什么重组范围不是只有Text?
从上面代码块看,text是通过by关键字再根据MutableState的代理调用getValue得到结果的,它是一个表达式,而Text则是一个函数,通过前面我们对函数使用表达式作为参数的调用关系了解,在调用函数之前会先确认表达式的结果,确认表达式的结果在这个例子中是在Button lambda中。所以能确认重组作用域在Button lambda

2. 为什么不是整个Foo发生重组

我们知道当State发生变化时才会发生重组,而Foo中没有访问任何State对象,所以它不会发生重组。让我们修改下代码看看,将by改成=,逻辑就清晰了

@Composable fun Foo() {
    val text: MutableState<String> = remember { mutableStateOf("") }
    println("TAG: Foo Scope")
    Button(onClick = { text.value = "${text.value}\n${text.value}" }) {
        println("TAG: Button lambada Scope")
        Text(text.value).also {
            println("TAG: Text Scope")
        }
    }
}

显而易见,text一直都是同一个MutableState对象,修改的只不过是它内部持有的value,而value只在Text中访问,所以Foo不会发生重组。

3. 为什么不是Button发生重组

因为Foo没发生重组,所以重组时并不会调用Button函数

4. onClick lambada表达式呢

重组范围只能是在可重组函数中,即使handlers,ButtononClick都不是可重组函数。所以这种函数不在重组范围内。

重组中 Inline 的坑

前面也提到了,inline函数是将代码拷贝,所以共享调用者的重组范围。在Compose中很多常见的widget被修饰成inline函数的,比如Column,Row,Box等。

总结

  1. Jetpack Compose是通过重组来刷新UI的
  2. 不依赖State的可重组函数不参与重组(因为没有观察和通知触发重组)
  3. 重组范围只能是在可重组函数中
  4. inline可重组函数共享调用者作用域

原文地址:Scoped recomposition in Jetpack Compose — what happens when state changes?