本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
前言
重组是 Compose 的一个重要特征,重组过程中 Composable 函数会对参数进行比较,如果参数没有发生变化则会跳过重组,即所谓的“智能”的重组。但是这个参数的比较不全是 runtime 的事情,Compiler 也会参与其中。
@Composable fun Foo(bar: String) { Text(bar) }
我们拿上面这个非常简单的 Composable 作例子,它经过编译后变成下面这样(反编译后的伪代码,看着更清晰):
@Composable fun Foo(bar: String, $composer: Composer<*>, $changed: Int) { $composer.startRestartGroup(405544596) var $dirty = $changed if ($changed and 14 === 0) { $dirty = $dirty or if ($composer.changed(x)) 2 else 4 } if ($dirty and 11 !== 2 || !$composer.skipping) { Text(bar) } else { $composer.skipToGroupEnd() } $composer.endRestartGroup().updateScope { Foo(bar, $changed) } }
大多数的 Composable 函数编译后都会被包装在 startRestartGroup/endRestartGroup
中,让当前函数有了重组的能力,可以看到函数结尾处 updateScope
注册的 lambda 就是用于重组时的递归调用。
为实现“智能”的重组,函数执行允许 skip ,此时会直接执行下面的 else 分支,skipToGroupEnd()
将对 SlotTable 的遍历推进到最后:
if ($dirty and 11 !== 2 || !$composer.skipping) { Text(bar) } else { $composer.skipToGroupEnd() }
这里 if 条件中依赖对 $dirty
的判断,而 $dirty
来自 $changed
。 那这些变量代表什么呢?另外,代码中出现了好多魔数,诸如 14 ,11, 2, 4 之类的,这些又代表什么呢?本文就来讨论一下这些内容。
$changed 与 ParamState
Composable 经过编译后函数签名会发生变化。除了新增 $composer
以外,还会添加 �ℎ�����参数。前缀‘changed参数。前缀‘ 告诉我们这些都是 Compiler 的产物。
$changed` 可以为参数提供额外的辅助信息,这些信息辅助运行时的参数比较,减少运行时的判断,提升性能。
$changed
是一个 Int 类型 (32bits),最低位是保留位,用来表示是否强制重组。然后从低到高,每3bits 代表一个参数的信息,这样 32 bit 至多可以承载 10 个参数的信息 ( 10 * 3 + 1 = 31 )。如果 Composable 参数超过 10 个,那么相应地会在签名中插入 $changed
, $changed1
... 以此类推。
这 3bits 信息被称为 ParamState
,它是一个 Enum,有 5 个取值所以需要占用 3 个 bit
enum class ParamState(val bits: Int) { Uncertain(0b000), Same(0b001), Different(0b010), Static(0b011), Unknown(0b100) }
- Uncertain(0b000) :参数最新值与最后一次重组的值比较,是否有变化不确定
- Same(0b001) :参数最新值与最后一次重组的值比较,没有发生变化
- Different(0b010) :参数最新值与最后一次重组的值比较,发生了变化
- Static(0b011): 参数是一个静态常量
- Unknown(0b100): 3bits 的最高位表示参数类型是否稳定,1 表示不稳定
Composable 函数在调用处会根据编译期静态分析的结果,设置最适当的 $changed
值。 例如下面代码中, App 中传入 Foo 的是一个静态值,所以 $changed
是 Static(0b011)
@Composable fun App() { Foo("Hello world!") } //编译后: @Composable fun MyApp($composer: Composer<*>) { Foo("Hello world!", $composer, 0b0110) //static(0b011) shl 1 + 0b1 }
在例如下面代码中,App 中传入 Foo 是一个变量,所以编译期无法确定类型,传入 Uncertain(0b000)
var str = "" @Composable fun App() { Foo(str) } //编译后: @Composable fun MyApp($composer: Composer<*>) { Foo("Hello world!", $composer, 0b0000) //Uncertain(0b000) shl 1 + 0b1 }
$dirty 与 ParamState.Uncertain
当 $changed
是 Uncertain
时,没法确定此次参数有没有变化,此时就需要运行时进行参数比较,经比较后的到一个确定结果 - 要么是 Same(0b001)
要么 Different(0b010)
,并更新到 $dirty
对应的字段。
var $dirty = $changed if ($changed and 14 === 0) { $dirty = $dirty or if ($composer.changed(x)) 4 else 2 }
14 二进制是 0b1110,所以上面 if
语句中 $changed and 14 === 0
的条件,只有 $changed
是 Uncertain
(0b000 左移一位) 时才成立。
当命中 Uncertain
时,调用 $composer.changed(x)
拿当前参数与 SlotTable 中的记录进行比较,如果有变化则则返回 false, 并将最新的参数存入 SlotTable。因此 4 就是 0b010 左移 1 位的结果,对应 Different
; 同理,2 对应的就是 Same
。
参数比较的结果会更新到 $dirty
用于后续判断是否参与重组。这里可能有人会问为什么要用 or
进行更新,直接赋值不就好了? 别忘了 $dirty
和 $changed
至多可以承载十个参数状态,我们这个例子只有一个参数看不出来 or
的意义,当有多个参数,就需要 or 去合并多个参数状态了。
ParamState.Same 与 ParamState.Different
前面讲了,一个 Uncertain
状态经过比较可以转换为 Same
或 Different
。如果 $changed
初始就是 Same
或 Different
,则意味着要么跳过重组,要么参与重组,总之行为是 Certaiin
的,因此需再进行参数比较了,参数值也不必存入 SlotTable 了,这样可以节省一些比较的开销以及 SlotTablle 的内存。
更新后的 $dirty
用来判断是否参与重组:
if ($dirty and 11 !== 2 || !$composer.skipping) { Text(bar) } else { $composer.skipToGroupEnd() }
11 二进制表示是 0b1011, 2 是 0b10, 所以只有 Different
和 UnKnown
符合条件。
- Different : 0b0100 and 0b1011 != 2
- Same:0b0010 and 0b1011 = 2
- Static: 0b0110 and 0b1011 = 2
- UnKnown:0b1000 and 0b101 != 2
Different
会对函数体进行重组,Same
或 Static
则跳过重组。 Unknown
虽然也符合条件,但是 Compiler 针对类型稳定性有其他优化,后文会看到。
ComposableFunctionBodyTransformer
上面的这些代码生成逻辑都是在 ComposableFunctionBodyTransformer
中实现的,这是 Compose Compiler 中最复杂的一个文件,今后我们再慢慢介绍,这里只看一下 $changed
的代码的生成部分,主要在 visitRestartableComposableFunction
函数内:
private fun visitRestartableComposableFunction( declaration: IrFunction, scope: Scope.FunctionScope, changedParam: IrChangedBitMaskValue, defaultParam: IrDefaultBitMaskValue? ): IrStatement { //... //是否可以跳过重组 canSkipExecution = buildPreambleStatementsAndReturnIfSkippingPossible( ... ) // if it has non-optional unstable params, the function can never skip, so we always // execute the body. Otherwise, we wrap the body in an if and only skip when certain // conditions are met. val dirtyForSkipping = if (dirty.used && dirty is IrChangedBitMaskVariable) { skipPreamble.statements.addAll(0, dirty.asStatements()) dirty } else changedParam val transformedBody = if (canSkipExecution) { //是否应该执行重组 var shouldExecute = irOrOr( dirtyForSkipping.irHasDifferences(scope.usedParams), irNot(irIsSkipping()) ) //... //生成执行重组或跳过重组的 if...else 代码块 irIfThenElse( condition = shouldExecute, thenPart = irBlock( statements = bodyPreamble.statements + transformed.statements ), // Use end offsets so that stepping out of the composable function // does not step back to the start line for the function. elsePart = irSkipToGroupEnd(body.endOffset, body.endOffset), startOffset = body.startOffset, endOffset = body.endOffset ) } else irComposite( statements = bodyPreamble.statements + transformed.statements ) //... }
canSkipExecution
表示是否可以跳过重组,党可以跳过重组时,生成下面这样的 if ... else
代码:
if ($dirty and 11 !== 2 || !$composer.skipping) { Text(bar) } else { $composer.skipToGroupEnd() }
irIfThenElse
用来生成 if...else
代码, shouldExecute
是 if 里的 $dirty and 11 !== 2 || !$composer.skipping
判断。 thenPart
和 elsePart
分别生成对应花括号里的代码。
这段代码告诉我们,如果 canSkipExecution 为 false,压根就不会生成上面的 if...else
的判断逻辑,一定会执行 Text(bar)
. 那么 canSkipExecution
是如何被赋值的呢?我们从 buildPreambleStatementsAndReturnIfSkippingPossible
里找一下实现
parameters.forEachIndexed { slotIndex, param -> val stability = stabilityOf(param.varargElementType ?: param.type) stabilities[slotIndex] = stability val isRequired = param.defaultValue == null val isUnstable = stability.knownUnstable() val isUsed = scope.usedParams[slotIndex] //... if (isUsed && isUnstable && isRequired) { // if it is a used + unstable parameter with no default expression, the fn // will _never_ skip mightSkip = false } }
buildPreambleStatementsAndReturnIfSkippingPossible
最终返回的是上面的 mightSkip
。也就是说当 Composable 的函数参数中,有任何一个参数是 isUsed && isUnstable && isRequired
,即 参数是不稳定类型、且没有默认值而且函数体中被使用,则当前 Composable 的重组就不应该跳过,无需生成 skipToGroupEnd
相关的 if...else
逻辑,减少运行开销和产物体积。
我们做个一个实验验证一下:
data class Bar(var str: String) @Composable fun Foo(bar: Bar) { Text(bar.str) }
上面的 Bar 就是一个不稳定类型。因此编译后的代码如下:
@Composable fun Foo(bar: Bar, $composer: Composer<*>, $changed: Int) { $composer.startRestartGroup(405544596) Text(bar.str) $composer.endRestartGroup().updateScope { Foo(bar, $changed) } }
果然,$dirty
以及 skipToGroupEnd
相关的逻辑都没有了,100% 会执行重组。但是如果 Foo 中没有对依 bar 的读取,则即使 Bar 是不稳定类型,也会生成 skipToGroupEnd
的代码。
Bar 的类型稳定性来自 stabilityOf
,那么编译器怎么决定类型是是否稳定呢,这个留着我们下一篇文章再做介绍。
最后
最后做一个总结:Compose Compiler 编译期提为参数提供了 ParamState 信息,可以减少无谓的参数比较,提升重组的性能。我们用下面的图做一个收尾: