D16/ 所以到底为什麽 remember 是 composable function? - @Composable 是什麽 part 2

今天大概会聊到的范围

  • compose runtime
  • compose compiler

今天会更深入的研究 Compose 在执行过程中,发生了什麽事情、内部的资料如何存放的。大部分的资讯可能都不会在平常开发 Compose 的时候用到。
很建议大家可以看看 Leland Richardson 在 Compose 还是 alpha 的时候,在 Android Dev Summit '19 的 talk。大部分的理解都是因为这个影片(和对应的文章)才看懂的

前情提要

在上一篇有说到,Composable 最初的呼叫点是 Composer.invokeComposableComposer 内部的运作,是会将 composable 一一存放在一个阵列中(想像的阵列中)。在每次 recomposition 时,透过重头走访的方式,来判断每个阵列中的物件是否需要被修改。一但需要被修改,compoer 就会将阵列尾端的空位移动至目前 cursor 所在处,并重新 compose (insert) 後续的物件。

产生空阵列 依序填入资料
重新走访时,可能修改参数 当需要改变 composable 时,将尾端空位提前 在空位中放进新的资料

上一篇也有提到,这样的存放资料方式,是为了让每次做新增、删除、修改的处理都能是固定时间的。唯独移动空位是最耗效能的。但也因为移动空位的发生机率较低(大多时候动态的是资料),且需要发生画面异动时往往也会一次异动一整个区块,因此这样的设计是最有效率的。

实际上怎麽执行?

当一个 Composable 要执行时,追根溯源最终会在 Composer 的 invokeComposable function 中,被强转成一般的 function 执行

// class ComposerImpl

internal fun invokeComposable(composer: Composer, composable: @Composable () -> Unit) {
    @Suppress("UNCHECKED_CAST")
    val realFn = composable as Function2<Composer, Int, Unit>
    realFn(composer, 1)
}

source

值得注意的是,composable function 被转成一般 function  时,这个 function 多了两个参数:一个 Composer 一个 Int。其实  composable function 在 compile 时,compose compiler 会偷偷将这个 function 增加这两个参数。

参数加上 Composer

// 在 ComposableFunctionBodyTransformer 中可以看到的说明

@Composable
fun A(x: Int) {
    f(x)
}

// getting transformed into

@Composable
fun A(x: Int, $composer: Composer<*>, $changed: Int) {
    $composer.startRestartGroup()
    // ...
    f(x)
    $composer.endRestartGroup()?.updateScope { next -> A(x, next, $changed or 0b1) }
}

source
ComposableFunctionBodyTransformer 需要依赖  ComposerParamTransformer 先执行过,  ComposerParamTransformer 会将 function 加入 composer 参数 source

执行时写入 Group Key

执行 invokeComposable 的是 doCompose 这个 function。在执行 invokeComposable 的前後,会先 start group 并且提供一个 key。在执行完 Composable 後,会 end group。

// Composer#doCompose

startGroup(invocationKey, invocation)
invokeComposable(this, content)
endGroup()

source

这边带入了一个 group 的概念,其实每个 composable function 都会被一个 group 框起来,并且每个 group 都会有一个 key。

讲简单一点

爬 source code 还没有很理解,让我们直接转化成 sample code 和图示

假设我们有一个计数器,点击 text 会让数数字 +1

@Composable
fun Counter() {

    var count by remember { mutableStateOf(0) }

    Text(text = "count: $count", modifier = Modifier.clickable { count += 1 })

}

可以想像他会在背景被加上两个参数:composer 和 key

@Composable
fun Counter( $composer:Composer, $key: Int) {        // <-- input composer and key

    var count by remember { mutableStateOf(0) }

    Text(text = "count: $count", modifier = Modifier.clickable { count += 1 })

}

在整个 composable 的最开始与最後,会对 composer 呼叫 start ( start group ) 与 end ( end group ),并给他 key

@Composable
fun Counter( $composer:Composer, $key: Int) {
    $composer.start($key)     // <-- start group with $key
    var count by remember { mutableStateOf(0) }

    Text(text = "count: $count", modifier = Modifier.clickable { count += 1 })
    $composer.end()          // <-- end group with $key
}

这个动作会穿透给一个 composable,composer 会一路被传下去、并替每个 composable 定义一个不同的 key ( 这边用 123, 456 代替 )

@Composable
fun Counter( $composer:Composer, $key: Int) {
    $composer.start($key)     
    var count by remember($composer, key = 123) { mutableStateOf(0) }    // <-- composer 会传递给 child composable

    Text(text = "count: $count", modifier = Modifier.clickable { count += 1 }, $composer, key = 456)  
    $composer.end()       
}

当今天 Composer 在走访整个 composable 的时候

  1. $composer.start 时,会先将 $key 写入阵列中
  2. 走访到 remember,会将 remember 的 group 写在阵列中(并带有 key = 123 )
  3. 并且将 remember 这个 composable 的参数与回传写进阵列中 ( 这边没有参数,回传为 state )
  4. 接着走到 Text,一样会先写一个 group ( key = 456 )
  5. Text 的参数写进阵列中 ( text / modifier )
  6. Text 内部可能还会有别的 composable,他们会递回的产生一个又一个的 Group

基本上, Composer 透过一个一维的资料结构,存放了树状 composable 资料结构。

再举个例子,如果我们的 Composable 中有条件判断

@Composable
fun App() {
    val result = getData()
    
    if (result == null) {
        Loading()
    } else {
        Page(result)
    }
}

在加入 composer 时,compose compiler 会分析整个 function 中,有可能有变动的地方,在不同的 branch 加上不同的 group。

@Composable
fun App($composer) {
    val result = getData()
    
    if (result == null) {
        $composer.start(123)    // group 123
        Loading()
        $composer.end()
    } else {
        $composer.start(456)    // group 456
        Page(result)
        $composer.end()
    }
}

假设第一次 result 真的是 null,整个资料阵列会是这个样子的:

https://ithelp.ithome.com.tw/upload/images/20210930/20141597oFy85VFGom.png

但当我第二次进来,假设 getData 有值时,Composer 会在走到 if-else 这边取得 Group(456)。Composer 发现之前存的 Group key 和期待的不同时,会将尾端的空位移到目前这个 Group 的位置,并且开始重新 insert 後续的 element

https://ithelp.ithome.com.tw/upload/images/20210930/20141597Hi8y5xrrnk.png

Positional Memorization

其实 Composer 还藏了另一个武器,被称为 Positional Memorization。他其实很像 locale cache,composer 会将参数与运算的资料与运算结果一同放在列表中。当重新走访时,Composer 会确认目前的参数是否与这个 Group 的参数一样,若一样,Composer 将不会花力气去运算,而是直接回传运算结果。

这边有个在研究这段时发现的有去事实。

一直以为是 remember 这个 function 在帮我记录 state。但是其实 remember 的 lambda 只会在首次 composition 时执行一次,在後续的 recomposition 不会再次执行。若希望再次执行,要在呼叫 remember lambda 时,带入 key 。

 remember("key") {  ... } 

带入 key 的 remember 会在 Group 的第一格写入 key , 第二格写入 remember lambda 执行的结果。当key 改变时,才会重新触发 lambda 内的运算。

我们目前的用法,其实是依赖 mutableStateOf 所产生出来的 MutableStateMutableState 会产生一个 Snapshot,当 Snapshot 改变时触发 recompistion。所以在 recompistion 时,Composer 拿出来的 mutableState 其实是同一个,是里面的 value 不同

小结

回到最初的问题,到底 @Composable 做了什麽,为什麽 remember 等各种 function 都是 @Composable 呢?答案可以简化成:@Composable function 是一个种可以被 Composer 认得,并解纪录的元件。这些元件的参数改变时,将会触发後续的 composable tree 被重新绘制。

越研究这个主题,越觉得自己跳进了一个无底洞。回答了一个 remember 的问题,却产生更多问题还没有好好被回答到:为什麽 LocalDensity.current 也是 composable? mutableState 怎麽触发 recomposition 的?

虽然这些对使用 Compose 来说可能真的没什麽帮助,但研究後却被这个新的 framework 精妙的设计所震憾,也在爬 source code 的过程学到了很多。


Reference:


<<:  [Day 15] ML 实验管理 — 翻开覆盖的陷阱卡~ 记帐小本本!

>>:  Day15 第十五天才介绍学习路径是否搞错什麽

Day37 参加职训(机器学习与资料分析工程师培训班),ResNet, RNN

ResNet架构 from tensorflow.keras import Input, Model...

解决宝塔强制绑定账号

现在新版本的宝塔面板强制绑定宝塔官网账号,否则就无法继续使用面板。 临时的解决办法就是我们在URL目...

[Day 29] - React 前端串後端 - 查询订单

今天在测试的时候发生了一个笑话, 发查询订单的request到丰收款的api结果一直回"验...

【Day 14】jQuery基本语法

jQuery的基本语法: 1.前面都会有美元 $ 的符号 2. $(CSS选择器).执行的动作() ...

Unity与Photon的新手相遇旅途 | Day11-敌人攻击

今天的内容为该如何简单制作出一个自动攻击的敌人 ...