本文为稀土技能社区首发签约文章,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参数。前缀‘changed 参数。前缀 ` 告知咱们这些都是 Compiler 的产品。$changed` 可认为参数供给额外的辅佐信息,这些信息辅佐运行时的参数比较,削减运行时的判别,提高功能。

$changed 是一个 Int 类型 (32bits),最低位是保留位,用来表明是否强制重组。然后从低到高,每3bits 代表一个参数的信息,这样 32 bit 至多能够承载 10 个参数的信息 ( 10 * 3 + 1 = 31 )。假如 Composable 参数超越 10 个,那么相应地会在签名中插入 $changed, $changed1 … 以此类推。

深入浅出 Compose Compiler(4) 智能重组与 $changed 参数

这 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 的是一个静态值,所以 $changedStatic(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

$changedUncertain 时,没法确认此次参数有没有改变,此刻就需要运行时进行参数比较,经比较后的到一个确认成果 – 要么是 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 的条件,只要 $changedUncertain (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 状况经过比较能够转换为 SameDifferent。假如 $changed 初始便是 SameDifferent,则意味着要么越过重组,要么参加重组,总之行为是 Certaiin 的,因此需再进行参数比较了,参数值也不用存入 SlotTable 了,这样能够节省一些比较的开支以及 SlotTablle 的内存。

更新后的 $dirty 用来判别是否参加重组:

    if ($dirty and 11 !== 2 || !$composer.skipping) {
        Text(bar)
    } else {
       $composer.skipToGroupEnd()
    }

11 二进制表明是 0b1011, 2 是 0b10, 所以只要 DifferentUnKnown 契合条件。

  • Different : 0b0100 and 0b1011 != 2
  • Same:0b0010 and 0b1011 = 2
  • Static: 0b0110 and 0b1011 = 2
  • UnKnown:0b1000 and 0b101 != 2

Different 会对函数体进行重组,SameStatic 则越过重组。 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 判别。 thenPartelsePart 别离生成对应花括号里的代码。

这段代码告知咱们,假如 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 信息,能够削减无谓的参数比较,提高重组的功能。咱们用下面的图做一个收尾:

深入浅出 Compose Compiler(4) 智能重组与 $changed 参数