前言
重组是 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
以外,还会添加 <math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mi>c</mi><mi>h</mi><mi>a</mi><mi>n</mi><mi>g</mi><mi>e</mi><mi>d</mi><mtext>参数。前缀</mtext><mi mathvariant="normal">‘</mi></mrow><annotation encoding="application/x-tex">changed 参数。前缀 </annotation></semantics></math>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 信息,可以减少无谓的参数比较,提升重组的性能。我们用下面的图做一个收尾:
作者:fundroid
链接:https://juejin.cn/post/7171253940087390221
网友评论