Compose 技术原理 如何确定重组范围

前言

习惯了 Android 开发的同学一定知道,UI 的刷新要非常的慎重,尤其是复杂的页面,这会给性能带来一定的损耗。Compose 中有一个非常重要的改念-重组(Recomposition),今天我们就来了解一下重组的规则。

官方文档中有一段对重组范围的说明,大意是如果界面的某些部分无效,Compose 会尽力只重组需要更新的部分。https://developer.android.com/jetpack/compose/mental-model#skips

请大家仔细看下面的例子,思考第一次运行时输出的日志是什么?Button 响应点击事件之后,日志输出又是什么?

@Composable
fun ReCompositionTest() {
    var text by remember { mutableStateOf("test") }
    Log.d("TAG", "ReCompositionTest: ")

    Button(onClick = {
        Log.d("TAG", "Button onClick: ")
        text = "onClick"
    }, content = {
        Log.d("TAG", "Button content: ")
        Text(text = text)
    })

    Text1(text)
    Text2(text)
    Text3()
}

@Composable
fun Text1(text: String) {
    Log.d("TAG", "Text1: ")
    Text(text)
}

@Composable
fun Text2(text: String){
    Log.d("TAG", "Text2: ")
    Text("Text2")
}

@Composable
fun Text3(){
    Log.d("TAG", "Text3: ")
    Text("Text3")
}

重组规则

我们先来看下上面例子的日志输出:
首次运行:
在这里插入图片描述
从代码中我们可以看出,日志的输出是符合顺序执行预期,这里要注意的是 Text2(text: String) 这个方法,在方法体内我们并没有使用传进来的参数。

第一次点击按钮:
在这里插入图片描述
当我们点击按钮之后,先出发了按钮的 onCLick,我们将 text = “onClick” ,然后进入重组阶段,ReCompositionTest 方法先被调用,随后第一个进行重组的方法是Button 的 content ,content 也是一个 @Composable 方法,所以也在重组的范围内。第二个进行重组的方法是 Text1 方法,然后此次重组结束。

第二次点击按钮:
在这里插入图片描述
第三次点击按钮:
在这里插入图片描述

接下来无论怎样点击按钮,日志输出的只有 onClick 事件了,不在有@Composable 参与重组了。这与官方文档上说的行为一致,Compose 会尽力只重组需要更新的部分。

当前例子中,需要更新的判断依据就是参数 var text 这个参数。在第一次点击按钮的时候,我们改变了 text 的值,此时依赖 text 参数的 @Composalbe 有 Button 的 content 和 Text1(),因此参与了重组。第二次点击按钮,text 的新值与旧值相等,所以没有任何 @Composalbe 方法参与重组。
注:实际上这里 Text1 Text2 Text3 都是执行了的,后续会对原因进行说明,并不影响当前分析结果。

于是我们有了一个初步的假设,只有受到 state 变化影响的代码块才会参与重组,不依赖state的代码不参与重组。也就是说更新总是尽可能的在最小范围内进行,但是有个地方需要特别注意,这里我们将代码稍作改动。

重组原理

Compose 的编译阶段一定是对我们的代码做了一些改变,不然依靠上面的代码,无法实现当前的结果,我们查看一下编译后的文件,Compose 编译后的文件在 build/tmp/kotlin-classes 路径下,但是当前的文件我们还是无法直接查看的,我们需要另一个工具 jadx,安装后将文件拖拽到工具即可。

    @Composable
    public static final void ReCompositionTest(@Nullable Composer $composer, int $changed) {
        // ...
        if ($changed == 0 && $composer2.getSkipping()) {
            $composer2.skipToGroupEnd();
        } else {
            // ...
            MutableState text$delegate = (MutableState) mutableState;
            Log.d(LiveLiterals.MainActivityKt.INSTANCE.String$arg-0$call-d$fun-ReCompositionTest(), LiveLiterals.MainActivityKt.INSTANCE.String$arg-1$call-d$fun-ReCompositionTest());
            // ...
            boolean invalid$iv$iv = $composer2.changed(text$delegate);
            Object it$iv$iv2 = $composer2.rememberedValue();
            if (invalid$iv$iv || it$iv$iv2 == Composer.Companion.getEmpty()) {
                Function0 function02 = new ReCompositionTest.1.1(text$delegate);
                $composer2.updateRememberedValue(function02);
                function0 = function02;
            } else {
                function0 = it$iv$iv2;
            }
            // ...
            ButtonKt.Button((Function0) function0, (Modifier) null, false, (MutableInteractionSource) null, (ButtonElevation) null, (Shape) null, (BorderStroke) null, (ButtonColors) null, (PaddingValues) null, ComposableLambdaKt.composableLambda($composer2, -819895718, true, new ReCompositionTest.2(text$delegate)), $composer2, 805306368, 510);
            Text1(m0ReCompositionTest$lambda1(text$delegate), $composer2, 0);
            Text2(m0ReCompositionTest$lambda1(text$delegate), $composer2, 0);
            Text3($composer2, 0);
        }
        // ..
    }
    
    @Composable
    public static final void Text1(@NotNull String text, @Nullable Composer $composer, int $changed) {
        // ...
        int $dirty = $changed;
        if (($changed & 14) == 0) {
            $dirty |= $composer2.changed(text) ? 4 : 2;
        }
        if ((($dirty & 11) ^ 2) != 0 || !$composer2.getSkipping()) {
            Log.d(LiveLiterals.MainActivityKt.INSTANCE.String$arg-0$call-d$fun-Text1(), LiveLiterals.MainActivityKt.INSTANCE.String$arg-1$call-d$fun-Text1());
            TextKt.Text-fLXpl1I(text, (Modifier) null, 0L, 0L, (FontStyle) null, (FontWeight) null, (FontFamily) null, 0L, (TextDecoration) null, (TextAlign) null, 0L, 0, false, 0, (Function1) null, (TextStyle) null, $composer2, 14 & $dirty, 0, 65534);
        } else {
            $composer2.skipToGroupEnd();
        }
        // ...
    }
    
    @Composable
    public static final void Text3(@Nullable Composer $composer, int $changed) {
        // ..
        if ($changed != 0 || !$composer2.getSkipping()) {
            Log.d(LiveLiterals.MainActivityKt.INSTANCE.String$arg-0$call-d$fun-Text3(), LiveLiterals.MainActivityKt.INSTANCE.String$arg-1$call-d$fun-Text3());
            TextKt.Text-fLXpl1I(LiveLiterals.MainActivityKt.INSTANCE.String$arg-0$call-Text$fun-Text3(), (Modifier) null, 0L, 0L, (FontStyle) null, (FontWeight) null, (FontFamily) null, 0L, (TextDecoration) null, (TextAlign) null, 0L, 0, false, 0, (Function1) null, (TextStyle) null, $composer2, 0, 0, 65534);
        } else {
            $composer2.skipToGroupEnd();
        }
        // ..
    }

我们重点来看一下关于参数相关的代码,隐藏掉其他逻辑。编译后的代码中所有的方法都增加了一些参数。Compose 在编译期间分析出受到 state 变化影响的代码块,当 state 发生变化的时候,会根据应用找到这些代码块并标记为 Invalid,在下一次渲染来到之前 Compose 触发重组,并执行 invalid 代码块。

Text1() 对 changed 进行了判断,然后进行更新。因为 Text3 不依赖任何外部的 state 变化,所以编译后的代码,调用处 changed 直接是 0 。composer 的作用非常的关键,后续文章中做详细介绍。

结论

Just don’t rely on side effects from recomposition and compose will do the right thing – Compose Team

关于重组,官方文档中并没有多少说明,Compose 在编译期保证了重组按照最合理的方式运行,开发者并不需要过多的关心。另外,我们要保证将副作用代码使用LaunchedEffect{}、DisposableEffect{}等 api 处理,保证 Composable 的“纯洁性”。

猜你喜欢

转载自blog.csdn.net/z2008q/article/details/128566222