今天大概会聊到的范围
- compose runtime
- compose compiler
今天会更深入的研究 Compose 在执行过程中,发生了什麽事情、内部的资料如何存放的。大部分的资讯可能都不会在平常开发 Compose 的时候用到。
很建议大家可以看看 Leland Richardson 在 Compose 还是 alpha 的时候,在 Android Dev Summit '19 的 talk。大部分的理解都是因为这个影片(和对应的文章)才看懂的
在上一篇有说到,Composable
最初的呼叫点是 Composer.invokeComposable
。Composer
内部的运作,是会将 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)
}
值得注意的是,composable function 被转成一般 function 时,这个 function 多了两个参数:一个 Composer 一个 Int。其实 composable function 在 compile 时,compose compiler 会偷偷将这个 function 增加这两个参数。
// 在 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
执行 invokeComposable 的是 doCompose 这个 function。在执行 invokeComposable 的前後,会先 start group 并且提供一个 key。在执行完 Composable 後,会 end group。
// Composer#doCompose
startGroup(invocationKey, invocation)
invokeComposable(this, content)
endGroup()
这边带入了一个 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 的时候
$composer.start
时,会先将 $key 写入阵列中remember
,会将 remember
的 group 写在阵列中(并带有 key = 123 )remember
这个 composable 的参数与回传写进阵列中 ( 这边没有参数,回传为 state )Text
,一样会先写一个 group ( key = 456 )Text
的参数写进阵列中 ( text
/ modifier
)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,整个资料阵列会是这个样子的:
但当我第二次进来,假设 getData 有值时,Composer 会在走到 if-else 这边取得 Group(456)。Composer 发现之前存的 Group key 和期待的不同时,会将尾端的空位移到目前这个 Group 的位置,并且开始重新 insert 後续的 element
其实 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
所产生出来的MutableState
。MutableState
会产生一个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 实验管理 — 翻开覆盖的陷阱卡~ 记帐小本本!
ResNet架构 from tensorflow.keras import Input, Model...
现在新版本的宝塔面板强制绑定宝塔官网账号,否则就无法继续使用面板。 临时的解决办法就是我们在URL目...
今天在测试的时候发生了一个笑话, 发查询订单的request到丰收款的api结果一直回"验...
jQuery的基本语法: 1.前面都会有美元 $ 的符号 2. $(CSS选择器).执行的动作() ...
今天的内容为该如何简单制作出一个自动攻击的敌人 ...