Jetpack Compose重组的效果域

几个月前, 我开端在出产级运用中运用Jetpack Compose, 当然是在编写了一些”Jetpack Compose Hello World项目”作为示例运用之后, 当然之后我抛弃了一切这些项目. 在出产级运用中运用Jetpack Compose适当具有挑战性, 由于咱们需要真正了解咱们正在编写的内容, 以及它对功能和优化的影响, 并保证它不会重现意想不到的行为/错误, 虽然在底层有很多东西咱们或许还不知道Jetpack Compose是怎么处理的, 其中之一便是重组.

重组是当输入发生改变时再次调用可组合函数的进程.

函数的输入发生改变时, 就会发生这种情况. 当Compose依据新的输入进行重组时, 它只会调用或许已发生改变的函数或lambdas, 而越过其他函数或lambdas. 经过越过一切参数未发生改变的函数或lambdas, Compose能够高效地进行重组.

简略地说, 重组便是从头渲染Compose组件的进程, 由于输入发生了改变, 咱们需要向用户显现最新的输入. 假如咱们经常在安卓视图上创立自界说视图, 咱们能够运用invalidate()函数从头渲染整个自界说视图组件. 重组与invalidate()的功能相似, 都是从头渲染当时视图, 但重组的作业方式更智能, 只重组输入发生改变的组件, 而越过其他组件, 这使得Jetpack Compose中的重组比Android视图中的invalidate()更有效.

Jetpack Compose是如何决定哪块代码进行重组的?

invalidate() vs recomposition

当咱们运用Compose构建用户界面时, 重组是不行避免的, 由于重组是Compose将当时数据状况更新到用户界面的机制, 但咱们能够消除不必要的重组, 以优化咱们的Compose代码. 经过了解重组的作业原理, 咱们能够保证自己编写的代码不会导致Compose重复进行不必要的重组.

重组效果域

什么是重组效果域?

重组效果域是能够独立重组的最小代码块. 它意味着什么? 这意味着编译器能够找到状况或输入读取的方位, 并只从头编译组件的最小效果域, 而无需从头编译一切组件. 让咱们看看下面的重组效果域示例:

Jetpack Compose是如何决定哪块代码进行重组的?

重组效果域

重组效果域通常以开头和结尾的函数括号符号, 在上面的示例中, 有两个重组效果域, 即Greeting效果域Button效果域. 在层次结构中, 咱们能够看到Button效果域也在Greeting效果域的内部, 但在重组效果域中, Greeting效果域Button效果域能够独立重组, Compose能够重组Greeting效果域而不重组Button效果域, 也能够重组Button效果域而不重组Greeting效果域, 或者在一个重组进程中同时重组这两个效果域.

为了了解重组效果域是怎么作业的, 让咱们做一些实验.

榜首部分 – 了解重组效果域

在开端之前, 咱们需要创立一个函数来盯梢重组, 这个函数将在每次重组发生时调用, 并核算已经调用了多少次重组. 咱们需要让这个函数内联, 以保证这个可组合函数没有自己的重组效果域.

class Ref(var value: Int)
@Composable
inline fun LogCompositions(msg: String) {
    val ref = remember { Ref(0) }
    SideEffect { ref.value++ }
    Log.d("RecompositionLog", "Compositions: $msg ${ref.value}")
}

在这一部分中, 咱们将了解重组效果域是怎么作业的. 让咱们看看下面的示例, 当咱们在Android Studio上创立编撰活动时, 咱们修正了默许的”Greeting”屏幕, 以展现重组效果域是怎么作业的.

@Composable
fun Greeting() {
    var state by remember {
        mutableStateOf("Hi Foo")
    }
    LogCompositions(msg = "Greeting Scope")
    Text(text = state)
    Button(
        onClick = { state = "Hi Foo ${Random.nextInt()}" },
        modifier = Modifier
            .padding(top = 32.dp)
    ) {
        LogCompositions(msg = "Button Scope")
        Text(
            text = "Click Me!"
        )
    }
}

在这个示例中, 咱们创立了一个文本和一个按钮, 只要点击按钮, 就会更新文本读取的状况. 这里有两个组成效果域:”Greeting效果域”和”Button效果域”. 正如我之前解释过的, 这两个效果域都能够独立履行. 让咱们试着运转这个示例.

Jetpack Compose是如何决定哪块代码进行重组的?

成果:Greeting效果域

“Greeting效果域”被调用, 但”Button效果域”没有被调用. 为什么? 由于state是在第7行读取的, 而该LoC是 Greeting效果域的一部分. 为什么Button效果域没有被调用? 由于在Button效果域上没有任何组成组件读取可调查的状况.

使状况声明更接近调用者

在上一个示例中, Compose重组了”Greeting效果域”, 成果是每次点击按钮时都会调用”Greeting效果域”的 LogComposition. 假如咱们移动状况和LogComposition的次序呢? 会不会由于咱们在LogComposition行之后才界说和读取状况而导致LogComposition不被调用? 让咱们试试看.

@Composable
fun Greeting() {
    LogCompositions(msg = "Greeting Scope")
    var state by remember {  //We move this line of code after log recomposition and closer to its caller and 
        mutableStateOf("Hi Foo")
    }
    Text(text = state)
    Button(
        onClick = { state = "Hi Foo ${Random.nextInt()}" },
        modifier = Modifier
            .padding(top = 32.dp)
    ) {
        LogCompositions(msg = "Button Scope")
        Text(
            text = "Click Me!"
        )
    }
}

成果:

Jetpack Compose是如何决定哪块代码进行重组的?

成果:Greeting效果域–使状况更接近调用者

成果保持不变, 由于Compose不是按代码行调用的, 而是按组成效果域调用的, 其中LogRecomposition和Text在同一个组成效果域中读取状况, 移动次序不会发生任何影响.

Jetpack Compose是如何决定哪块代码进行重组的?

将读取状况的组件移到另一个效果域

现在让咱们把读取状况的组件移到另一个效果域, 从Greeting效果域移到Button效果域如下所示:

@Composable
fun Greeting() {
    var state by remember {
        mutableStateOf("Hi Foo")
    }
    LogCompositions(msg = "Greeting Scope")
    Text(text = "Hi Foo")
    Button(
        onClick = { state = "Hi Foo ${Random.nextInt()}" },
        modifier = Modifier
            .padding(top = 32.dp)
    ) {
        LogCompositions(msg = "Button Scope")
        Text(
            text = state
        )
    }
}

成果:

Jetpack Compose是如何决定哪块代码进行重组的?

成果 : Greeting效果域 – 将状况移动到另一个组成效果域

是的, 正如预期的那样:Button效果域被调用, 由于咱们将读取状况的组件移动到了Button效果域. 虽然依据层次结构, Button效果域 也是Greeting效果域 的一部分, 但Compose能够调用它而不调用其父效果域组件. 要进一步了解这种跳转进程, 您能够阅览更多关于”Donute Hole Skipping” 的文章.

第二部分 – 内联可组合函数

咱们在此添加新组件, 即Column. 我相信大多数人在Jetpack Compose中创立用户界面时都会用到这个组件. 在咱们尝试运转这个示例之前, 依据榜首部分的示例, 这段代码将只调用Column效果域, 对吗? 由于在第9行读取状况的组件坐落Column效果域. 让咱们来看看.

@Composable
fun Greeting() {
    var state by remember {
        mutableStateOf("Hi Foo")
    }
    LogCompositions(msg = "Greeting Scope")
    Column { // We add new component here
        LogCompositions(msg = "Column Scope")
        Text(text = state)
        Button(
            onClick = { state = "Hi Foo ${Random.nextInt()}" },
        ) {
            LogCompositions(msg = "Button Scope")
            Text(
                text = "Click Me"
            )
        }
    }
}

成果:

Jetpack Compose是如何决定哪块代码进行重组的?

成果:内联可组合函数

好吧, 这是不应该发生的. Column效果域被调用了, 但为什么Greeting效果域也被调用了呢? 要回答这个问题, 让咱们翻开Column的界说

Jetpack Compose是如何决定哪块代码进行重组的?

内联Column函数

Column(以及Compose中的大多数容器, 如方框、行、束缚布局)都是内联函数. 咱们知道, 当咱们将内联函数编译到 Java 代码中时, 该函数并不真正存在, 它指示编译器将完好的主体函数带给其调用者, 而无需创立新函数. 这就使得内联可编译函数(如Column)没有自己的重构效果域, 而是遵从父效果域. 这便是为什么在上面的示例中,Column效果域Greeting效果域一起被调用, 由于Column效果域实际上并不存在.

您能够在 Kotlin 官方文档或这篇文章中阅览更多关于内联函数的内容:Kotlin中的内联函数

本文的扼要说明

Jetpack Compose是如何决定哪块代码进行重组的?

内联函数与非内联函数

在对第1部分和第2部分进行抽查后, 我有一个问题:

Jetpack Compose是如何决定哪块代码进行重组的?

“假如重组在效果域组件内重组了悉数代码, 那么为什么咱们不能将每一个组件分块/包裹成更小的效果域来取得更小的重组效果域呢?”

在回答这个问题之前, 让咱们跳到第三部分.

第三部分–重构效果域和可越过组件

Android Studio为Jetpack Compose提供了布局查看器(Layout Inspector), 有了这个布局查看器, 咱们就能够盯梢重组的调用次数, 并查看哪些组件被重组或越过.

运用布局查看器获取重组次数*

Jetpack Compose是如何决定哪块代码进行重组的?

Layout Inspector

让咱们修正代码示例, 现在咱们添加了新的状况和读取状况的组件, 但该状况永久不会发生改变, 让咱们看看下面的代码:

@Composable
fun Greeting() {
    var state by remember {
        mutableStateOf("Hi Foo")
    }
    var staticState by remember {
        mutableStateOf("This state never changes")
    }
    LogCompositions(msg = "Greeting Scope")
    Column {
        LogCompositions(msg = "Column Scope")
        Text(text = state)
        Text(text = staticState)
        Button(
            onClick = { state = "Hi Foo ${Random.nextInt()}" },
        ) {
            LogCompositions(msg = "Button Scope")
            Text(
                text = "Click Me"
            )
        }
    }
}

在第6行, 咱们有一个名为staticState的状况. 这个状况是可变的, 但永久不会发生改变, 这个状况在第13行被文本组件读取. 因而, 咱们有两个状况, 并在同一效果域内被两个文本组件读取. 现在让咱们运转上面的代码, 比较LogComposition和Layout Inspector的成果.

Jetpack Compose是如何决定哪块代码进行重组的?

LogComposition和布局查看器的成果

咱们能够看到, 榜首个文本和第二个文本坐落同一效果域, 即”Greeting效果域”. 每次咱们点击按钮时都会调用该Greeting效果域, 但它只会更新state变量, 而不会更新statisState.

请仔细调查咱们的布局查看器, 即使调用了Greeting效果域上的悉数代码, 金豪也知道第二个文本读取的状况没有更新, 因而金豪直接越过了从头组成这个组件. 经过越过不必要的从头编译组件, Compose能够高效地作业.

现在咱们知道, 没有必要对重组效果域进行分块或将每个组件封装到更小的重组效果域中, 更重要的是保证重组效果域中的每个组件都是可越过的, 因而即使在单个重组效果域中存在很多组件, 也不会影响功能, 由于Compose只重组必要的组件, 其他组件则不会重组.

要取得可越过和不行越过组件的完好报告, 咱们能够运用Composable衡量指标或第三方库, 如Mendable.

结论

  • Compose经过调用读取状况的最小重组效果域进行智能重组
  • 与其忧虑一个效果域中的代码行, 不如保证重组效果域中的组件是可越过的, 这将提高Compose的作业效率.
  • 一般来说, 在不知道函数是否内联的情况下, 很难确定哪些效果域将被重组, 因而应运用辅助工具(如Layout Inspector)来盯梢重组情况.
  • 出于优化意图, 重组规则或许会在未来的开发中发生改变