前言
习惯了 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 的“纯洁性”。