深入详解 Jetpack Compose | 实现原理
@Composable 注解意味着什么?
// 函数声明
suspend fun MyFun() { … }
// lambda 声明
val myLambda = suspend { … }
// 函数类型
fun MyFun(myParam: suspend () -> Unit) { … }
// 函数声明
@Composable fun MyFun() { … }
// lambda 声明
val myLambda = @Composable { … }
// 函数类型
fun MyFun(myParam: @Composable () -> Unit) { … }
fun Example(a: () -> Unit, b: suspend () -> Unit) {
a() // 允许
b() // 不允许
}
suspend
fun Example(a: () -> Unit, b: suspend () -> Unit) {
a() // 允许
b() // 允许
}
fun Example(a: () -> Unit, b: @Composable () -> Unit) {
a() // 允许
b() // 不允许
}
@Composable
fun Example(a: () -> Unit, b: @Composable () -> Unit) {
a() // 允许
b() // 允许
}
执行模式
Gap Buffer
https://en.wikipedia.org/wiki/Gap_buffer
现在,我们可以进行插入操作了。
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(
text="Count: $count",
onPress={ count += 1 }
)
}
fun Counter($composer: Composer) {
$composer.start(123)
var count by remember { mutableStateOf(0) }
Button(
text="Count: $count",
onPress={ count += 1 }
)
$composer.end()
}
fun Counter($composer: Composer) {
$composer.start(123)
var count by remember($composer) { mutableStateOf(0) }
Button(
$composer,
text="Count: $count",
onPress={ count += 1 },
)
$composer.end()
}
当此 composer 执行时,它会进行以下操作:
Composer.start 被调用并存储了一个组对象 (group object)
remember 插入了一个组对象
mutableStateOf 的值被返回,而 state 实例会被存储起来
Button 基于它的每个参数存储了一个分组
最后,当我们到达 composer.end 时:
现在,所有这些组对象已经占据了很多的空间,它们为什么要占据这些空间呢?这些组对象是用来管理动态 UI 可能发生的移动和插入的。编译器知道哪些代码会改变 UI 的结构,所以它可以有条件地插入这些分组。大部分情况下,编译器不需要它们,所以它不会向插槽表 (slot table) 中插入过多的分组。为了说明一这点,请您查看以下条件逻辑:
@Composable fun App() {
val result = getData()
if (result == null) {
Loading(...)
} else {
Header(result)
Body(result)
}
}
fun App($composer: Composer) {
val result = getData()
if (result == null) {
$composer.start(123)
Loading(...)
$composer.end()
} else {
$composer.start(456)
Header(result)
Body(result)
$composer.end()
}
}
Positional Memoization (位置记忆化)
@Composable
fun App(items: List<String>, query: String) {
val results = items.filter { it.matches(query) }
// ...
}
函数第二次执行时,remember 函数会查看新传入的值并将其与旧值进行对比,如果所有的值都没有发生改变,过滤操作就会在跳过的同时将之前的结果返回。这便是位置记忆化。
有趣的是,这一操作的开销十分低廉: 编译器必须存储一个先前的调用。这一计算可以发生在您的 UI 的各个地方,由于您是基于位置对其进行存储,因此只会为该位置进行存储。
下面是 remember 的函数签名,它可以接收任意多的输入与一个 calculation 函数。
@Composable
fun <T> remember(vararg inputs: Any?, calculation: () -> T): T
不过,这里没有输入时会产生一个有趣的退化情况。我们可以故意误用这一 API,比如记忆一个像 Math.random 这样不输出稳定结果的计算:
@Composable fun App() {
val x = remember { Math.random() }
// ...
}
使用全局记忆化来进行这一操作将不会有任何意义,但如果换做使用位置记忆化,此操作将最终呈现出一种新的语义。每当我们在 Composable 层级中使用 App 函数时,都将会返回一个新的 Math.random 值。不过,每次 Composable 被重新组合时,它将会返回相同的 Math.random 值。这一特性使得持久化成为可能,而持久化又使得状态成为可能。
存储参数
下面,让我们用 Google Composable 函数来说明 Composable 是如何存储函数的参数的。这个函数接收一个数字作为参数,并且通过调用 Address Composable 函数来绘制地址。
@Composable fun Google(number: Int) {
Address(
number=number,
street="Amphitheatre Pkwy",
city="Mountain View",
state="CA"
zip="94043"
)
}
@Composable fun Address(
number: Int,
street: String,
city: String,
state: String,
zip: String
) {
Text("$number $street")
Text(city)
Text(", ")
Text(state)
Text(" ")
Text(zip)
}
Compose 将 Composable 函数的参数存储在插槽表中。在本例中,我们可以看到一些冗余: Address 调用中添加的 "Mountain View" 与 "CA" 会在下面的文本调用被再次存储,所以这些字符串会被存储两次。
我们可以在编译器级为 Composable 函数添加 static 参数来消除这种冗余。
fun Google(
$composer: Composer,
$static: Int,
number: Int
) {
Address(
$composer,
0b11110 or ($static and 0b1),
number=number,
street="Amphitheatre Pkwy",
city="Mountain View",
state="CA"
zip="94043"
)
}
本例中,static 参数是一个用于指示运行时是否知道参数不会改变的位字段。如果已知一个参数不会改变,则无需存储该参数。所以这一 Google 函数示例中,编译器传递了一个位字段来表示所有参数都不会发生改变。
接下来,在 Address 函数中,编译器可以执行相同的操作并将参数传递给 text。
fun Address(
$composer: Composer,
$static: Int,
number: Int, street: String,
city: String, state: String, zip: String
) {
Text($composer, ($static and 0b11) and (($static and 0b10) shr 1), "$number $street")
Text($composer, ($static and 0b100) shr 2, city)
Text($composer, 0b1, ", ")
Text($composer, ($static and 0b1000) shr 3, state)
Text($composer, 0b1, " ")
Text($composer, ($static and 0b10000) shr 4, zip)
}
这些位操作逻辑难以阅读且令人困惑,但我们也没有必要理解它们: 编译器擅长于此,而人类则不然。
在 Google 函数的实例中,我们看到这里不仅有冗余,而且有一些常量。事实证明,我们也不需要存储它们。这样一来,number 参数便可以决定整个层级,它也是唯一一个需要编译器进行存储的值。
有赖于此,我们可以更进一步,生成可以理解 number 是唯一一个会发生改变的值的代码。接下来这段代码可以在 number 没有发生改变时直接跳过整个函数体,而我们也可以指导 Composer 将当前索引移动至函数已经执行到的位置。
fun Google(
$composer: Composer,
number: Int
) {
if (number == $composer.next()) {
Address(
$composer,
number=number,
street="Amphitheatre Pkwy",
city="Mountain View",
state="CA"
zip="94043"
)
} else {
$composer.skip()
}
}
Composer 知道快进至需要恢复的位置的距离。
重组
为了解释重组是如何工作的,我们需要回到计数器的例子:
fun Counter($composer: Composer) {
$composer.start(123)
var count = remember($composer) { mutableStateOf(0) }
Button(
$composer,
text="Count: ${count.value}",
onPress={ count.value += 1 },
)
$composer.end()
}
编译器为 Counter 函数生成的代码含有一个 composer.start 和一个 compose.end。每当 Counter 执行时,运行时就会理解: 当它调用 count.value 时,它会读取一个 appmodel 实例的属性。在运行时,每当我们调用 compose.end,我们都可以选择返回一个值。
$composer.end()?.updateScope { nextComposer ->
Counter(nextComposer)
}
接下来,我们可以在该返回值上使用 lambda 来调用 updateScope 方法,从而告诉运行时在有需要时如何重启当前的 Composable。这一方法等同于 LiveData 接收的 lambda 参数。在这里使用问号的原因——可空的原因——是因为如果我们在执行 Counter 的过程中不读取任何模型对象,则没有理由告诉运行时如何更新它,因为我们知道它永远不会更新。
最后
推荐阅读