引言
Compose 的绘制有三个阶段,组合 > 布局 > 绘制。后两个进程与传统视图的烘托进程相近,唯一组合是 Compose 所特有的。Compose 经过组合生成烘托树,这是 Compose 结构的核心才能,而这个进程首要是依靠 SlotTable 完结的,本文就来介绍一下 SlotTable 体系。
1. 从 Compose 烘托进程说起
依据 Android 原生视图的开发进程,其本质便是构建一棵依据 View 的烘托树,当帧信号到达时从根节点开端深度遍历,顺次调用 measure/layout/draw,直至完结整棵树的烘托。对于 Compose 来说也存在这样一棵烘托树,咱们将其称为 Compositiion,树上的节点是 LayoutNode,Composition 经过 LayoutNode 完结 measure/layout/draw 的进程终究将 UI 显现到屏幕上。Composition 依托 Composable 函数的履行来创立以及更新,即所谓的组合和重组。
例如上面的 Composable 代码,经过履行后会生成右侧的 Composition。
一个函数经过履行是怎么转换成 LayoutNode 的呢?深化 Text 的源码后发现其内部调用了 Layout, Layout 是一个能够自界说布局的 Composable,咱们直接运用的各类 Composable 终究都是经过调用 Layout 来完结不同的布局和显现效果。
//Layout.kt
@Composable inline fun Layout(
content: @Composable () -> Unit,
modifier: Modifier = Modifier,
measurePolicy: MeasurePolicy
) {
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val viewConfiguration = LocalViewConfiguration.current
ReusableComposeNode<ComposeUiNode, Applier<Any>>(
factory = ComposeUiNode.Constructor,
update = {
set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
set(density, ComposeUiNode.SetDensity)
set(layoutDirection, ComposeUiNode.SetLayoutDirection)
set(viewConfiguration, ComposeUiNode.SetViewConfiguration)
},
skippableUpdate = materializerOf(modifier),
content = content
)
}
Layout 内部经过 ReusableComposeNode 创立 LayoutNode。
-
factory
便是创立 LayoutNode 的工厂 -
update
用来记载会更新 Node 的状况用于后续烘托
持续进入 ReusableComposeNode :
//Composables.kt
inline fun <T, reified E : Applier<*>> ReusableComposeNode(
noinline factory: () -> T,
update: @DisallowComposableCalls Updater<T>.() -> Unit,
noinline skippableUpdate: @Composable SkippableUpdater<T>.() -> Unit,
content: @Composable () -> Unit
) {
//...
$composer.startReusableNode()
//...
$composer.createNode(factory)
//...
Updater<T>(currentComposer).update()
//...
$composer.startReplaceableGroup(0x7ab4aae9)
content()
$composer.endReplaceableGroup()
$composer.endNode()
}
咱们知道 Composable 函数经过编译后会传入 Composer, 代码中依据传入的 Composer 完结了一系列操作,主逻辑很明晰:
-
Composer#createNode
创立节点 -
Updater#update
更新 Node 状况 -
content()
持续履行内部 Composable,创立子节点。
此外,代码中还穿插着了一些 startXXX/endXXX ,这样的成对调用就恰似对一棵树进行深度遍历时的压栈/出栈
startReusableNode
NodeData // Node数据
startReplaceableGroup
GroupData //Group数据
... // 子Group
endGroup
endNode
不仅仅 ResuableComposeNode 这样的内置 Composable,咱们自己写的 Composable 函数体经过编译后的代码也会刺进很多的 startXXX/endXXX,这些其实都是 Composer 对 SlotTable 拜访的进程,Composer 的功用便是经过对 SlotTable 的读写来创立和更新 Composition。
下图是 Composition,Composer 与 SlotTable 的关系类图
2. 初识 SlotTable
前文咱们将 Composable 履行后生成的烘托树称为 Compositioin。其实更精确来说,Composition 中存在两棵树,一棵是 LayoutNode 树,这是真实履行烘托的树,LayoutNode 能够像 View 相同完结 measure/layout/draw 等详细烘托进程;而另一棵树是 SlotTable,它记载了 Composition 中的各种数据状况。 传统视图的状况记载在 View 目标中,在 Compose 面向函数编程而不面向目标,所以这些状况需求依托 SlotTable 进行管理和保护。
Composable 函数履行进程中产生的所有数据都会存入 SlotTable, 包括 State、CompositionLocal,remember 的 key 与 value 等等 ,这些数据不随函数的出栈而消失,能够跨过重组存在。Composable 函数在重组中假如产生了新数据则会更新 SlotTable。
SlotTable 的数据存储在 Slot 中,一个或多个 Slot 又归属于一个 Group。能够将 Group 了解为树上的一个个节点。说 SlotTable 是一棵树,其实它并非真实的树形数据结构,它用线性数组来表达一棵树的语义,从 SlotT able 的界说中能够看到这一点:
//SlotTable.kt
internal class SlotTable : CompositionData, Iterable<CompositionGroup> {
/**
* An array to store group information that is stored as groups of [Group_Fields_Size]
* elements of the array. The [groups] array can be thought of as an array of an inline
* struct.
*/
var groups = IntArray(0)
private set
/**
* An array that stores the slots for a group. The slot elements for a group start at the
* offset returned by [dataAnchor] of [groups] and continue to the next group's slots or to
* [slotsSize] for the last group. When in a writer the [dataAnchor] is an anchor instead of
* an index as [slots] might contain a gap.
*/
var slots = Array<Any?>(0) { null }
private set
SlotTable 有两个数组成员,groups
数组存储 Group 信息,slots
存储 Group 所辖的数据。用数组替代结构化存储的优点是能够提高对“树”的拜访速度。 Compose 中重组的频率很高,重组进程中会不断的对 SlotTable 进行读写,而拜访数组的时刻复杂度只要 O(1),所以运用线性数组结构有助于提高重组的功能。
groups 是一个 IntArray,每 5 个 Int 为一组构成一个 Group 的信息
-
key
: Group 在 SlotTable 中的标识,在 Parent Group 范围内唯一 -
Group info
: Int 的 Bit 位中存储着一些 Group 信息,例如是否是一个 Node,是否包括 Data 等,这些信息能够经过位掩码来获取。 -
Parent anchor
: Parent 在 groups 中的方位,即相对于数组指针的偏移 -
Size: Group
: 包括的 Slot 的数量 -
Data anchor
:相关 Slot 在 slots 数组中的开始方位
slots 是真实存储数据的地方,Composable 履行进程中能够产生任意类型的数据,所以数组类型是 Any?
。每个 Gorup 相关的 Slot 数量不定,Slot 在 slots 中依照所属 Group 的顺序顺次寄存。
groups 和 slots 不是链表,所以当容量不足时,它们会进行扩容。
3. 深化了解 Group
Group 的效果
SlotTable 的数据存储在 Slot 中,为什么充任树上节点的单位不是 Slot 而是 Group 呢?由于 Group 供给了以下几个效果:
-
构建树形结构: Composable 初次履行进程中,在 startXXXGroup 中会创立 Group 节点存入 SlotTable,一起经过设置 Parent ahchor 构建 Group 的父子关系,Group 的父子关系是构建烘托树的根底。
-
辨认结构改变: 编译期刺进 startXXXGroup 代码时会依据代码方位生成可辨认的
$key
(parent 范围内唯一)。在初次组合时$key
会跟着 Group 存入 SlotTable,在重组中,Composer 依据$key
的比较能够辨认出 Group 的增、删或者方位移动。换言之,SlotTable 中记载的 Group 带着了方位信息,故这种机制也被称为 Positional Memoization。Positional Memoization 能够发现 SlotTable 结构上的改变,终究转化为 LayoutNode 树的更新。 -
重组的最小单位: Compose 的重组是“智能”的,Composable 函数或者 Lambda 在重组中能够跳过不必要的履行。在 SlotTtable 上,这些函数或 lambda 会被包装为一个个 RestartGroup ,因而 Group 是参加重组的最小单位。
Group 的类型
Composable 在编译期会生成多种不同类型的 startXXXGroup,它们在 SlotTable 中刺进 Group 的一起,会存入辅佐信息以完结不同的功用:
startXXXGroup | 说明 |
---|---|
startNode /startResueableNode | 刺进一个包括 Node 的 Group。例如文章最初 ReusableComposeNode 的比方中,显现调用了 startResueableNode ,然后调用 createNode 在 Slot 中刺进 LayoutNode。 |
startRestartGroup | 刺进一个可重复履行的 Group,它或许会跟着重组被再次履行,因而 RestartGroup 是重组的最小单元。 |
startReplacableGroup | 刺进一个能够被替换的 Group,例如一个 if/else 代码块便是一个 ReplaceableGroup,它能够在重组中被刺进后者从 SlotTable 中移除。 |
startMovableGroup | 刺进一个能够移动的 Group,在重组中或许在兄弟 Group 之间产生方位移动。 |
startReusableGroup | 刺进一个可复用的 Group,其内部数据可在 LayoutNode 之间复用,例如 LazyList 中同类型的 Item。 |
当然 startXXXGroup 不止用于刺进新 Group,在重组中也会用来追寻 SlotTable 的已有 Group,与当时履行 中的代码状况进行比较。接下来咱们看下几种不同类型的 startXXXGroup 呈现在什么样的代码中。
4. 编译期生成的 startXXXGroup
前面介绍了 startXXXGroup 的几种类型,咱们平日在写 Compose 代码时,对他们毫无感知,那么他们别离是在何种状况下生成的呢?下面看几种常见的 startXXXGroup 的生成时机:
startReplacableGroup
前面说到过 Positional Memoization 的概念,即 Group 存入 SlotTable 时,会带着依据方位生成的 $key
,这有助于辨认 SlotTable 的结构改变。下面的代码能更清楚地解说这个特性
@Composable
fun ReplaceableGroupTest(condition: Boolean) {
if (condition) {
Text("Hello") //Text Node 1
} else {
Text("World") //Text Node 2
}
}
这段代码,当 condition 从 true 变为 false,意味着烘托树应该移除旧的 Text Node 1 ,并增加新的 Text Node 2。源码中咱们没有为 Text 增加可辨识的 key,假如仅依照源码履行,程序无法辨认出 counditioin 改变前后 Node 的不同,这或许导致旧的节点状况仍然残留,UI 不符预期。
Compose 怎么解决这个问题呢,看一下上述代码编译后的样子(伪代码):
@Composable
fun ReplaceableGroupTest(condition: Boolean, $composer: Composer?, $changed: Int) {
if (condition) {
$composer.startReplaceableGroup(1715939608)
Text("Hello")
$composer.endReplaceableGroup()
} else {
$composer.startReplaceableGroup(1715939657)
Text("World")
$composer.endReplaceableGroup()
}
}
能够看到,编译器为 if/else 每个条件分支都刺进了 RestaceableGroup ,并增加了不同的 $key
。这样当 condition
产生改变时,咱们能够辨认 Group 产生了改变,从而从结构上改变 SlotTable,而不仅仅更新原有 Node。
if/else 内部即便调用了多个 Composable(比方或许呈现多个 Text) ,它们也只会包装在一个 RestartGroup ,由于它们总是被一同刺进/删去,无需单独生成 Group 。
startMovableGroup
@Composable
fun MoveableGroupTest(list: List<Item>) {
Column {
list.forEach {
Text("Item:$it")
}
}
}
上面代码是一个显现列表的比方。由于列表的每一行在 for 循环中生成,无法依据代码方位完结 Positional Memoization,假如参数 list 产生了改变,比方刺进了一个新的 Item,此时 Composer 无法辨认出 Group 的位移,会对其进行删去和重建,影响重组功能。
针对这类无法依托编译器生成 $key
的问题,Compose 给了解决方案,能够经过 key {...}
手动增加唯一索引 key,便于辨认 Item 的新增,提高重组功能。经优化后的代码如下:
//Before Compiler
@Composable
fun MoveableGroupTest(list: List<Item>) {
Column {
list.forEach {
key(izt.id) { //Unique key
Text("Item:$it")
}
}
}
}
上面代码经过编译后会刺进 startMoveableGroup:
@Composable
fun MoveableGroupTest(list: List<Item>, $composer: Composer?, $changed: Int) {
Column {
list.forEach {
key(it.id) {
$composer.startMovableGroup(-846332013, Integer.valueOf(it));
Text("Item:$it")
$composer.endMovableGroup();
}
}
}
}
startMoveableGroup 的参数中除了 GroupKey 还传入了一个辅佐的 DataKey。当输入的 list 数据中呈现了增/删或者位移时,MoveableGroup 能够依据 DataKey 辨认出是否是位移而非毁掉重建,提高重组的功能。
startRestartGroup
RestartGroup 是一个可重组单元,咱们在日常代码中界说的每个 Composable 函数都能够单独参加重组,因而它们的函数体中都会刺进 startRestartGroup/endRestartGroup,编译前后的代码如下:
// Before compiler (sources)
@Composable
fun RestartGroupTest(str: String) {
Text(str)
}
// After compiler
@Composable
fun RestartGroupTest(str: String, $composer: Composer<*>, $changed: Int) {
$composer.startRestartGroup(-846332013)
// ...
Text(str)
$composer.endRestartGroup()?.updateScope { next ->
RestartGroupTest(str, next, $changed or 0b1)
}
}
看一下 startRestartGroup 做了些什么
//Composer.kt
fun startRestartGroup(key: Int): Composer {
start(key, null, false, null)
addRecomposeScope()
return this
}
private fun addRecomposeScope() {
//...
val scope = RecomposeScopeImpl(composition as CompositionImpl)
invalidateStack.push(scope)
updateValue(scope)
//...
}
这儿首要是创立 RecomposeScopeImpl 并存入 SlotTable 。
-
RecomposeScopeImpl
中包裹了一个 Compsoable 函数,当它需求参加重组时,Compose 会从 SlotTable 中找到它并调用RecomposeScopeImpl#invalide()
标记失效,当重组来临时 Composable 函数被从头履行。 - RecomposeScopeImpl 被缓存到
invalidateStack
,并在Composer#endRestartGroup()
中回来。updateScope
为其设置需求参加重组的 Compsoable 函数,其实便是对当时函数的递归调用。注意 endRestartGroup 的回来值是可空的,假如 RestartGroupTest 中不依靠任何状况则无需参加重组,此时将回来 null。
可见,无论 Compsoable 是否有必要参加重组,生成代码都相同。这降低了代码生成逻辑的复杂度,将判别留到运行时处理。
5. SlotTable 的 Diff 与遍历
SlotTable 的 Diff
声明式结构中,烘托树的更新都是经过 Diff 完结的,比方 React 经过 VirtualDom 的 Diff 完结 Dom 树的部分更新,提高 UI 改写的功能。
SlotTable 便是 Compose 的 “VirtualDom”,Composable 初次履行时在 SlotTable 中刺进 Group 和对应的 Slot 数据。 当 Composable 参加重组时,依据代码现状与 SlotTable 中的状况进行 Diff,发现 Composition 中需求更新的状况,并终究运用到 LayoutNode 树。
这个 Diff 的进程也是在 startXXXGroup 进程中完结的,详细完结都会集在 Composer#start()
:
//Composer.kt
private fun start(key: Int, objectKey: Any?, isNode: Boolean, data: Any?) {
//...
if (pending == null) {
val slotKey = reader.groupKey
if (slotKey == key && objectKey == reader.groupObjectKey) {
// 经过 key 的比较,确定 group 节点没有改变,进行数据比较
startReaderGroup(isNode, data)
} else {
// group 节点产生了改变,创立 pending 进行后续处理
pending = Pending(
reader.extractKeys(),
nodeIndex
)
}
}
//...
if (pending != null) {
// 寻觅 gorup 是否在 Compositon 中存在
val keyInfo = pending.getNext(key, objectKey)
if (keyInfo != null) {
// group 存在,但是方位产生了改变,需求凭借 GapBuffer 进行节点位移
val location = keyInfo.location
reader.reposition(location)
if (currentRelativePosition > 0) {
// 对 Group 进行位移
recordSlotEditingOperation { _, slots, _ ->
slots.moveGroup(currentRelativePosition)
}
}
startReaderGroup(isNode, data)
} else {
//...
val startIndex = writer.currentGroup
when {
isNode -> writer.startNode(Composer.Empty)
data != null -> writer.startData(key, objectKey ?: Composer.Empty, data)
else -> writer.startGroup(key, objectKey ?: Composer.Empty)
}
}
}
//...
}
start 办法有四个参数:
-
key
: 编译期依据代码方位生成的$key
-
objectKey
: 运用key{}
增加的辅佐 key -
isNode
:当时 Gorup 是否是一个 Node,在 startXXXNode 中,此处会传入 true -
data
:当时 Group 是否有一个数据,在 startProviers 中会掺入 providers
start 办法中有很多对 reader 和 writer 的调用,稍后会对他们作介绍,这儿只需求知道他们能够追寻 SlotTable 中当时应该拜访的方位,并完结读/写操作。上面的代码现现已过提炼,逻辑比较明晰:
- 依据 key 比较 Group 是否相同(SlotTable 中的记载与代码现状),假如 Group 没有改变,则调用 startReaderGroup 进一步判别 Group 内的数据是否产生改变
- 假如 Group 产生了改变,则意味着 start 中 Group 需求新增或者位移,经过 pending.getNext 查找 key 是否在 Composition 中存在,若存在则表明需求 Group 需求位移,经过 slot.moveGroup 进行位移
- 假如 Group 需求新增,则依据 Group 类型,别离调用不同的 writer#startXXX 将 Group 刺进 SlotTable
Group 内的数据比较是在 startReaderGroup 中进行的,完结比较简单
private fun startReaderGroup(isNode: Boolean, data: Any?) {
//...
if (data != null && reader.groupAux !== data) {
recordSlotTableOperation { _, slots, _ ->
slots.updateAux(data)
}
}
//...
}
-
reader.groupAux
获取当时 Slot 中的数据与 data 做比较 - 假如不同,则调用
recordSlotTableOperation
对数据进行更新。
注意对 SlotTble 的更新并非立即收效,这在后文会作介绍。
SlotReader & SlotWriter
上面看到,start 进程中对 SlotTable 的读写都需求依托 Composition 的 reader 和 writer 来完结。
writer 和 reader 都有对应的 startGroup/endGroup 办法。对于 writer 来说 startGroup 代表对 SlotTable 的数据改变,例如刺进或删去一个 Group ;对于 reader 来说 startGroup 代表着移动 currentGroup 指针到最新方位。currentGroup
和 currentSlot
指向 SlotTable 当时拜访中的 Group 和 Slot 的方位。
看一下 SlotWriter#startGroup
中刺进一个 Group 的完结:
private fun startGroup(key: Int, objectKey: Any?, isNode: Boolean, aux: Any?) {
//...
insertGroups(1) // groups 中分配新的方位
val current = currentGroup
val currentAddress = groupIndexToAddress(current)
val hasObjectKey = objectKey !== Composer.Empty
val hasAux = !isNode && aux !== Composer.Empty
groups.initGroup( //填充 Group 信息
address = currentAddress, //Group 的刺进方位
key = key, //Group 的 key
isNode = isNode, //是否是一个 Node
hasDataKey = hasObjectKey, //是否有 DataKey
hasData = hasAux, //是否包括数据
parentAnchor = parent, //相关Parent
dataAnchor = currentSlot //相关Slot地址
)
//...
val newCurrent = current + 1
this.parent = current //更新parent
this.currentGroup = newCurrent
//...
}
-
insertGroups
用来在 groups 中分配刺进 Group 用的空间,这儿会涉及到 Gap Buffer 概念,咱们在后文会详细介绍。 -
initGroup
:依据 startGroup 传入的参数初始化 Group 信息。这些参数都是在编译期跟着不同类型的 startXXXGroup 生成的,在此处真实写入到 SlotTable 中 - 最终更新 currentGroup 的最新方位。
再看一下 SlotReader#startGroup
的完结:
fun startGroup() {
//...
parent = currentGroup
currentEnd = currentGroup + groups.groupSize(currentGroup)
val current = currentGroup++
currentSlot = groups.slotAnchor(current)
//...
}
代码十分简单,首要便是更新 currentGroup,currentSlot 等的方位。
SlotTable 经过 openWriter/openReader 创立 writer/reader,运用完毕需求调用各自的 close 封闭。reader 能够 open 多个一起运用,而 writer 同一时刻只能 open 一个。为了防止产生并发问题, writer 与 reader 不能一起履行,所以对 SlotTable 的 write 操作需求推迟到重组后进行。因而咱们在源码中看到很多 recordXXX 办法,他们将写操作提为一个 Change 记载到 ChangeList,等待组合完毕后再一起运用。
6. SlotTable 改变推迟收效
Composer 中运用 changes 记载改变列表
//Composer.kt
internal class ComposerImpl {
//...
private val changes: MutableList<Change>,
//...
private fun record(change: Change) {
changes.add(change)
}
}
Change
是一个函数,履行详细的改变逻辑,函数签名即参数如下:
//Composer.kt
internal typealias Change = (
applier: Applier<*>,
slots: SlotWriter,
rememberManager: RememberManager
) -> Unit
-
applier
: 传入 Applier 用于将改变运用到 LayoutNode 树,在后文详细介绍 Applier -
slots
:传入 SlotWriter 用于更新 SlotTable -
remembrerManger
:传入 RemeberManger 用来注册 Composition 生命周期回调,能够在特定时刻点完结特定业务,比方 LaunchedEffect 在初次进入 Composition 时创立 CoroutineScope, DisposableEffect 在从 Composition 中脱离时调用 onDispose ,这些都是经过在这儿注册回调完结的。
记载 Change
咱们以 remember{}
为例看一下 Change 怎么被记载。
remember{} 的 key 和 value 都会作为 Compositioin 中的状况记载到 SlotTable 中。重组中,当 remember 的 key 产生改变时,value 会从头核算 value 并更新 SlotTable。
//Composables.kt
@Composable
inline fun <T> remember(
key1: Any?,
calculation: @DisallowComposableCalls () -> T
): T {
return currentComposer.cache(currentComposer.changed(key1), calculation)
}
//Composer.kt
@ComposeCompilerApi
inline fun <T> Composer.cache(invalid: Boolean, block: () -> T): T {
@Suppress("UNCHECKED_CAST")
return rememberedValue().let {
if (invalid || it === Composer.Empty) {
val value = block()
updateRememberedValue(value)
value
} else it
} as T
}
如上是 remember 的源码
-
Composer#changed
办法中会读取 SlotTable 中存储的 key 与 key1 进行比较 -
Composer#cache
中,rememberedValue
会读取 SlotTable 中缓存的当时 value。 - 假如此时 key 的比较中发现了不同,则调用
block
核算并回来新的 value,一起调用updateRememberedValue
将 value 更新到 SlotTable。
updateRememberedValue 终究会调用 Composer#updateValue
,看一下详细完结:
//Composer.kt
internal fun updateValue(value: Any?) {
//...
val groupSlotIndex = reader.groupSlotIndex - 1 //更新方位Index
recordSlotTableOperation(forParent = true) { _, slots, rememberManager ->
if (value is RememberObserver) {
rememberManager.remembering(value)
}
when (val previous = slots.set(groupSlotIndex, value)) {//更新
is RememberObserver ->
rememberManager.forgetting(previous)
is RecomposeScopeImpl -> {
val composition = previous.composition
if (composition != null) {
previous.composition = null
composition.pendingInvalidScopes = true
}
}
}
}
//...
}
//记载更新 SlotTable 的 Change
private fun recordSlotTableOperation(forParent: Boolean = false, change: Change) {
realizeOperationLocation(forParent)
record(change) //记载 Change
}
这儿关键代码是对 recordSlotTableOperation
的调用:
- 将 Change 加入到 changes 列表,这儿 Change 的内容是经过 SlotWriter#set 将 value 更新到 SlotTable 的指定方位,
groupSlotIndex
是核算出的 value 在 slots 中的偏移量。 -
previous
回来 remember 的旧 value ,可用来做一些后处理。从这儿也能够看出, RememberObserver 与 RecomposeScopeImpl 等也都是 Compoisition 中的状况。- RememberObserver 是一个生命周期回调,RememberMananger#forgetting 对其进行注册,当 previous 从 Composition 移除时,RememberObserver 会收到通知
- RecomposeScopeImpl 是可重组的单元,
pendingInvalidScopes = true
意味着此重组单元从 Composition 中脱离。
除了 remember,其他涉及到 SlotTable 结构的改变,例如删去、移动节点等也会凭借 changes 推迟收效(刺进操刁难 reader 没有影响不大故会立即运用)。比方中 remember 场景的 Change 不涉及 LayoutNode 的更新,所以 recordSlotTableOperation 中没有运用到 Applier
参数。但是当种族形成 SlotTable 结构产生改变时,需求将改变运用到 LayoutNoel 树,这时就要运用到 Applier 了。
运用 Change
前面说到,被记载的 changes 等待组合完结后再履行。
当 Composable 初次履行时,在 Recomposer#composeIntial
中完结 Composable 的组合
//Composition.kt
override fun setContent(content: @Composable () -> Unit) {
//...
this.composable = content
parent.composeInitial(this, composable)
}
//Recomposer.kt
internal override fun composeInitial(
composition: ControlledComposition,
content: @Composable () -> Unit
) {
//...
composing(composition, null) {
composition.composeContent(content) //履行组合
}
//...
composition.applyChanges() //运用 Changes
//...
}
能够看到,紧跟在组合之后,调用 Composition#applyChanges()
运用 changes
。相同,在每次重组产生后也会调用 applyChanges。
override fun applyChanges() {
val manager = ...
//...
applier.onBeginChanges()
// Apply all changes
slotTable.write { slots ->
val applier = applier
changes.fastForEach { change ->
change(applier, slots, manager)
}
hanges.clear()
}
applier.onEndChanges()
//...
}
在 applyChanges 内部看到对 changes 的遍历和履行。 此外还会经过 Applier 回调 applyChanges 的开端和完毕。
7. UiApplier & LayoutNode
SlotTable 结构的改变是怎么反映到 LayoutNode 树上的呢?
前面咱们将 Composable 履行后生成的烘托树称为 Composition。其实 Composition 是对这一棵烘托树的宏观认知,精确来说 Compositoin 内部经过 Applier 保护着 LayoutNode 树并履行详细烘托。SlotTable 结构的改变会跟着 Change 列表的运用反映到 LayoutNode 树上。
像 View 相同,LayoutNode 经过 measure/layout/draw
等一系列办法完结详细烘托。此外它还供给了 insertAt/removeAt 等办法完结子树结构的改变。这些办法会在 UiApplier 中调用:
//UiApplier.kt
internal class UiApplier(
root: LayoutNode
) : AbstractApplier<LayoutNode>(root) {
override fun insertTopDown(index: Int, instance: LayoutNode) {
// Ignored
}
override fun insertBottomUp(index: Int, instance: LayoutNode) {
current.insertAt(index, instance)
}
override fun remove(index: Int, count: Int) {
current.removeAt(index, count)
}
override fun move(from: Int, to: Int, count: Int) {
current.move(from, to, count)
}
override fun onClear() {
root.removeAll()
}
}
UiApplier 用来更新和修正 LayoutNode 树:
-
down()/up()
用来移动 current 的方位,完结树上的导航。 -
insertXXX/remove/move
用来修正树的结构。其间insertTopDown
和insertBottomUp
都用来刺进新节点,仅仅刺进的方式有所不同,一个是自下而上一个是自顶而下,针对不同的树形结构挑选不同的刺进顺序有助于提高功能。例如 Android 端的 UiApplier 首要依托 insertBottomUp 刺进新节点,由于 Android 的烘托逻辑下,子节点的改变会影响父节点的从头 measure,自此向下的刺进能够防止影响太多的父节点,提高功能,由于 attach 是最终才进行。
Composable 的履行进程只依靠 Applier 笼统接口,UiApplier 与 LayoutNode 仅仅 Android 平台的对应完结,理论上咱们经过自界说 Applier 与 Node 能够打造自己的烘托引擎。例如 Jake Wharton 有一个名为 Mosaic 的项目,便是经过自界说 Applier 和 Node 完结了自界说的烘托逻辑。
Root Node的创立
Android 平台下,咱们在 Activity#setContent
中调用 Composable:
//Wrapper.android.kt
internal fun AbstractComposeView.setContent(
parent: CompositionContext,
content: @Composable () -> Unit
): Composition {
//...
val composeView = ...
return doSetContent(composeView, parent, content)
}
private fun doSetContent(
owner: AndroidComposeView,
parent: CompositionContext,
content: @Composable () -> Unit
): Composition {
//...
val original = Composition(UiApplier(owner.root), parent)
val wrapped = owner.view.getTag(R.id.wrapped_composition_tag)
as? WrappedComposition
?: WrappedComposition(owner, original).also {
owner.view.setTag(R.id.wrapped_composition_tag, it)
}
wrapped.setContent(content)
return wrapped
}
-
doSetContent
中创立 Composition 实例,一起传入了绑定 Root Node 的 Applier。Root Node 被AndroidComposeView
持有,来自 View 国际的 dispatchDraw 以及KeyEvent
,touchEvent
等便是从这儿经过 Root Node 传递到了 Compose 国际。 -
WrappedComposition
是一个装饰器,也是用来为 Composition 与 AndroidComposeView 建立连接,咱们常用的很多来自 Android 的 CompositionLocal 便是这儿构建的,比方LocalContext
,LocalConfiguration
等等。
8. SlotTable 与 Composable 生命周期
Composable 的生命周期能够归纳为以下三阶段,现在认识了 SlotTable 之后,咱们也能够从 SlotTable 的角度对其进行解说:
-
Enter
:startRestartGroup 中将 Composable 对应的 Gorup 存入 SlotTable -
Recompose
:SlotTble 中查找 Composable (by RecomposeScopeImple) 从头履行,并更新 SlotTable -
Leave
:Composable 对应的 Group 从 SlotTable 中移除。
在 Composable 中运用副效果 API 能够充任 Composble 生命周期回调来运用
DisposableEffect(Unit) {
//callback when entered the Composition & recomposed
onDispose {
//callback for leaved the Composition
}
}
咱们以 DisposableEffect 为例,看一下生命周期回调是怎么依据 SlotTable 体系完结的。 看一下 DispoableEffect 的完结,代码如下:
@Composable
@NonRestartableComposable
fun DisposableEffect(
key1: Any?,
effect: DisposableEffectScope.() -> DisposableEffectResult
) {
remember(key1) { DisposableEffectImpl(effect) }
}
private class DisposableEffectImpl(
private val effect: DisposableEffectScope.() -> DisposableEffectResult
) : RememberObserver {
private var onDispose: DisposableEffectResult? = null
override fun onRemembered() {
onDispose = InternalDisposableEffectScope.effect()
}
override fun onForgotten() {
onDispose?.dispose()
onDispose = null
}
override fun onAbandoned() {
// Nothing to do as [onRemembered] was not called.
}
}
能够看到,DisposableEffect 的本质便是运用 remember 向 SlotTable 存入一个 DisposableEffectImpl,这是一个 RemeberObserver 的完结。 DisposableEffectImpl 跟着父 Gorup 进入和脱离 SlotTable ,将接收到 onRemembered
和 onForgotten
的回调。
还记得前面讲过的 applyChanges 吗,它产生在重组完结之后
override fun applyChanges() {
val manager = ... // 创立 RememberManager
//...
// Apply all changes
slotTable.write { slots ->
//...
changes.fastForEach { change ->
//运用 changes, 将 ManagerObserver 注册进 RememberMananger
change(applier, slots, manager)
}
//...
}
//...
manager.dispatchRememberObservers() //分发回调
}
前面也说到,SlotTable 写操作中产生的 changes 将在这儿统一运用,当然也包括了 DisposableEffectImpl 刺进/删去时 record 的 changes,详细来说便是对 ManagerObserver 的注册,会在后边的 dispatchRememberObservers
中进行回调。
重组是达观的
官网文档中在介绍重组有这样一段话:重组是“达观”的
When recomposition is canceled, Compose discards the UI tree from the recomposition. If you have any side-effects that depend on the UI being displayed, the side-effect will be applied even if composition is canceled. This can lead to inconsistent app state.
Ensure that all composable functions and lambdas are idempotent and side-effect free to handle optimistic recomposition.
developer.android.com/jetpack/com…
很多人初看这段话会不明所以,但是在解读了源码之后相信能够了解它的意义了。这儿所谓 “达观” 是指 Compose 的重组总是假定不会被中止,一旦产生了中止,Composable 中履行的操作并不会真实反映到 SlotTable,由于经过源码咱们知道了 applyChanges 产生在 composiiton 成功完毕之后。
假如组合被中止,你在 Composable 函数中读取的状况很或许和终究 SlotTable 中的不一致。因而假如咱们需求依据 Composition 的状况进行一些副效果处理,必须要运用 DisposableEffect 这样的副效果 API 包裹,由于经过源码咱们也知道了 DisposableEffect 的回调是 applyChanges 履行的,此时能够确保重组现已完结,获取的状况与 SlotTable 相一致。
9. SlotTable 与 GapBuffer
前面介绍过,startXXXGroup 中会与 SlotTable 中的 Group 进行 Diff,假如比较不相等,则意味着 SlotTable 的结构产生了改变,需求对 Group 进行刺进/删去/移动,这个进程是依据 Gap Buffer 完结的。
Gap Buffer 概念来自文本编辑器中的数据结构,能够将它了解为线性数组中可滑动、可伸缩的缓存区域,详细到 SlotTable 中,便是 groups 中的未运用的区域,这段区域能够在 groups 移动,提高 SlotTble 结构改变时的更新功率,以下举例说明:
@Composable
fun Test(condition: Boolean) {
if (condition) {
Node1()
Node2()
}
Node3()
Node4()
}
SlotTable 初始只要 Node3,Node4,然后依据状况改变,需求刺进 Node1,Node2,这个进程中假如没有 Gap Buffer,SlotTable 的改变如下图所示:
每次刺进新 Node 都会导致 SlotTable 中已有 Node 的移动,功率低下。再看一下引进 Gap Buffer 之后的行为:
当刺进新 Node 时,会将数组中的 Gap 移动到待刺进方位,然后再开端刺进新 Node。再刺进 Node1,Node2 乃至它们的子 Node,都是在填充 Gap 的闲暇区域,不会影响形成 Node 的移动。 看一下移动 Gap 的详细完结,相关代码如下:
//SlotTable.kt
private fun moveGroupGapTo(index: Int) {
//...
val groupPhysicalAddress = index * Group_Fields_Size
val groupPhysicalGapLen = gapLen * Group_Fields_Size
val groupPhysicalGapStart = gapStart * Group_Fields_Size
if (index < gapStart) {
groups.copyInto(
destination = groups,
destinationOffset = groupPhysicalAddress + groupPhysicalGapLen,
startIndex = groupPhysicalAddress,
endIndex = groupPhysicalGapStart
)
}
//...
}
-
Index
是要刺进 Group 的方位,即需求将 Gap 移动到此处 -
Group_Fields_Size
是 groups 中单位 Gorup 的长度,现在是常量 5。
几个临时变量的意义也十分明晰:
-
groupPhysicalAddress
: 当时需求刺进 gorup 的地址 -
groupPhysicalGapLen
: 当时Gap 的长度 -
groupPhysicalGapStart
:当时Gap 的开始地址
当 index < gapState
时,需求将 Gap 前移到 index 方位为新刺进做准备。从后边紧跟的 copyInto
的参数可知,Gap 的前移实践是经过 group 后移完结的,即将 startIndex
处的 Node 复制到 Gap 的新方位之后 ,如下图:
这样咱们不需求真的移动 Gap,只要将 Gap 的 start 的指针移动到 groupPyhsicalAddress
即可,新的 Node1 将在此处刺进。当然,groups 移动之后,anchor 等相关信息也要进行相应的更新。
最终再看一下删去 Node 时的 Gap 移动状况,原理也是相似的:
将 Gap 移动到待删去 Group 之前,然后开端删去 Node,这样,删去进程其实便是移动 Gap 的 end 方位而已,功率很高而且确保了 Gap 的连续。
10. 总结
SlotTable 体系是 Compose 从组合到烘托到屏幕,整个进程中的最重要环节,结合下面的图咱们回顾一下整个流程:
- Composable 源码在编译期会被刺进 startXXXGroup/endXXXGroup 模板代码,用于对 SlotTable 的树形遍历。
- Composable 初次组合中,startXXXGroup 在 SlotTable 中刺进 Group 并经过 $key 辨认 Group 在代码中的方位
- 重组中,startXXXGroup 会对 SlotTable 进行遍历和 Diff,并经过 changes 推迟更新 SlotTble,一起运用到 LayoutNode 树
- 烘托帧到达时,LayoutNode 针对改变部分进行 measure > layout > draw,完结 UI 的部分改写。