写在最初

SlotTable是Compose的核心之一,这篇文章尝试着在资料很少、彻底不了解的状况下去探究一下SlotTable相关的内容。因而,本篇文章会十分注重思路,在阅览进程中,会有十分多的关于“考虑进程”的部分。

别的,看这个源码纯属猎奇好玩,对实践应用Compose几乎没有协助。

开端看之前,请确认你现已:

  • 熟练掌握kotlin的语法和Kotlin式的编程风格
  • 了解一些数据结构的根本知识
  • 能熟练运用Compose

当然,以上这些也能够在看文章的进程中边看边学习。

假设文中的图看不清,文章终究有高清大图。 传送

0 初识 SlotTable

或许你彻底没听过这个 SlotTable,所以咱们先来大约认识一下它。

SlotTable 是用于 Compose 实践寄存各种“数据”的结构。咱们能够经过 currentComposer.compositionData 取到 CompositionData 接口的方针,它实践上便是个 SlotTable。

好,现在你对 SlotTable 现已有了一个最模糊的认识了,相信此刻你必定会有各式各样的疑问:

  • 从大局来看,假设我在 Activity 的 onCreate 调用 setContent{} 结构了一个 Compose 编写的 Activity,那么它里面含有几个 SlotTable?
  • 既然之前说 SlotTable 是寄存“各种”数据的,那它的内部究竟寄存了哪些类型的数据?
  • 它的内部结构是怎么样的,它是怎么寄存数据的?
  • Compose 编写页面的办法十分灵敏,能够随意地进行条件判别或许循环,例如以下代码:
       @Composable
       fun test() {
           if (condition) {
               Text("ptq")
           }
       }
    
    那么,例如这种条件操控的@Composable 调用,会影响 SlotTable 的数据结构吗?
    • 进一步地,什么时分、谁、怎么导致 SlotTable 的数据产生改变?
    • 再进一步地,Compose 作为一个 UI 结构,假设是杂乱的 UI 页面,怎么确保数据改动的高功能?

初度触摸 SlotTable,大约能产生上面这些疑问,就让这些疑问来驱动咱们去持续往下深化探究吧。

开端探究之前,我先来打个退堂鼓,事实上,这篇文章里只会答复一部分上面的疑问。因为,以这篇文章的篇幅,实在没法讲清一切东西。假设你是想对Compose的原理有个大致了解,那这篇讲SlotTable源码的文章或许就不太合适了。

1 SlotTable 的结构

要想搞清楚 SlotTable 机制,第一件事,便是要搞清楚它的结构。

1.1 初识

那么接下来,咱们开端探究 SlotTable 的结构。首要来想办法瞄一眼这个 SlotTable,看下面的代码。

@Composable
fun Greeting() {
    var show by remember { mutableStateOf(false) }
    val composition = currentComposer.composition
    Button(onClick = { show = !show }) {
        if (show) {
            Text("show")
        }
    }
    LaunchedEffect(show) {
        launch {
            delay(500)
            composition.printSlotTable()
        }
    }
}

printSlotTable 是我自己写的一个扩展函数,便是运用反射把 SlotTable 打印出来,点进 SlotTable 的源码一看,发现他有三千多行,SlotTable 类的特点也是形形色色的,怎么办?定睛一看,发现里面有个叫asString的函数,它是 Compose 团队用来调试 SlotTable 的 dump 函数,而且注释还说不要直接 toString,因为既耗时又内容冗杂。那么这个 asString 就最合适咱们用来大致了解 SlotTable 了,来看看它输出了啥。

Jetpack Compose探索(一)——SlotTable
从这个输出,咱们能得到以下估测:

  1. 一个 SlotTable 是由若干个 Group 组成的。
  2. 从 Group 前的留白能够推出,SlotTable 是一个树状结构。
  3. 从我随便圈的一些当地能够看出,这个 SlotTable 还真是寄存了“各种”数据,有咱们的 remember(mutableState),有 LaunchedEffect,有 CompositionLocal 等等,还有各种 lambdaImpl。
  4. 还有几个值得重视的字段:
    • aux – 字面意思应该是辅佐数据,能够看到 remember、LaunchEffect 之类的老朋友。
    • slots – 字面意思是插槽,从这个命名来看,这应该是从属于 Group 的更小的数据结构,它应该是记载了实践的数据,例如 remember 作为辅佐信息记载在 aux 中,而 remember 的 mutableState 就放在 slots 里。

暂时就能推出这些信息,那么,现在咱们有了一个大约的认知之后,过家家完毕了。接下来就要开端盘一盘这 SlotTable 的 3000 多行代码了。

1.2 结构

来看看 SlotTable 的特点和概念。

这些特点、概念在SlotTable.kt的最初有一大段注释解说,可是直接看这些注释,关于刚开端探究SlotTable的咱们来说,太困难了,看得十分绕,许多概念都是相互引用相互解说,无从下手,因而,咱们绝不能操之过急,得先找到一个突破口。

1.2.1 groups: IntArray

在 1.1 节中咱们知道了 SlotTable 由 Group 构成,而正好,SlotTable 类的第一个特点便是 groups,只不过,它看起来是一个 int 数组,而非咱们之前想的一棵树?非也。它是以数组来标明的一棵树,一般状况下这么干是为了功能。

因为 groups 不是一个专门的 Group 类的 array,而是一个 IntArray,那么单个 group 必定也是以 int 标明的了。下面看看单个的 group。

也便是说,假设正常来编写这个Group的代码,咱们是会界说一个Group类,然后一切的group方针寄存在Group[]数组中,可是,Group应该会是运用上很频繁、实例数量许多的一个类,因而,为了功能,规划者采用了int的办法来表征Group类的字段,而Group自身也不再以一个专门的类来存储,而是由接连寄存的若干int值组成。

1.2.1.1 Group

一个 group 由 5 个 int 字段构成,分别为 key、groupInfo、parentAnchor、size 和 dataAnchor。

  • 至于这些字段是什么意思,咱们现在能大约猜想一下:
    • key – 用于标识这个 group
    • groupInfo – 用于记载 group 自身的信息?
    • parentAnchor – 父节点的锚点?锚点便是方位的意思,也便是说记载了父 group 在整个 groups 数组中的方位?
    • size – group 的巨细?
    • dataAnchor – 关于 group 内的实践数据寄存的方位信息?(也便是说,groups应该仅仅寄存group自身信息的数组,而实践寄存group里面的数据——也便是slots的当地应该在另一处,所以后续需求拜访某个group的详细数据时,应该是先在groups数组里查到dataAnchor的信息,再依据这个dataAnchor索引去另一处拜访详细数据

因为满是 int 存储,咱们拜访 group 的内容是十分不便的,从注释能够知道,现已写好了一堆扩展函数来便利地拜访 group 的字段,咱们随便找几个例子看看。

//依据address获取groupInfo
private fun IntArray.groupInfo(address: Int): Int =
    this[address * Group_Fields_Size + GroupInfo_Offset]
//这个group是否有aux信息
private fun IntArray.hasAux(address: Int) =
    this[address * Group_Fields_Size + GroupInfo_Offset] and Aux_Mask != 0

咱们先不看上面这段详细的代码,留意到呈现了几个大写的变量,实践上他们是一些界说,这些界说关于咱们去了解结构是十分有协助的。

关于组的常量

  • 组的构成

    Jetpack Compose探索(一)——SlotTable

    1. key 便是 startGroup 中传入的仅有 key
    2. 就和咱们之前说的相同,一个组用 5 个 int 标明,而例如 groupInfo 这种,显着是一个 int 放不下的,因而这儿又采用了咱们了解的位运算来存储信息。
  • GroupInfo 的构成(共占 1 个 int,32 位)

    Jetpack Compose探索(一)——SlotTable

    各个字段的意义

    • n:为 1 则这个 group 代表一个 node
    • ks:为 1 则这个 group 有一个 object key slot
    • ds:为 1 则这个 group 有一个 aux slot
    • m:为 1 则这个 group 被符号
    • cm:为 1 则这个 group 有一个符号
    • 其他低位全代表 node count

    至于这些字段是什么意思,暂时先不管,凭自己的认知先猜猜,有个大约的形象就行,咱们往下看。

1.2.1.2 寻址

好,持续回来,到这儿停止,咱们的使命是解读拜访 Group 字段的代码,为了实在能搞懂 Group 乃至后文 SlotTable 中各种操作相关的代码,咱们还需求再了解一个东西,便是寻址

SlotTable 的数组拜访的规划中,有四个重要概念——Index、Address、Anchor 和 Gap。

Index 和 Address 看起来比较好了解,从英文意思来看,都是索引方位的意思,Anchor 是锚点的意思,应该也是相似于索引方位,可是带有“符号”这层意义。而 Gap 则意为空隙。

因为 group 在 groups 数组中的刺进移动删去等操作,或许会导致 groups 数组中 group 与 group 之间产生空隙,这个空隙便是 gap,它本质上是一段接连的数组区间,寄存的值满是 null。

那么,这四个人物的联系是什么呢?经过阅览源码,我总结出了下面这张图。这张图十分重要,它是后边正确了解代码的根底。

Jetpack Compose探索(一)——SlotTable

咱们以 groups 数组为例,假设有 100 个 group,gapLen=2。

  • 能够看到,Index 和 Address 是两个设想的序列——“设想”的意思是他们并不存在于物理内存中。实在存在物理内存中的序列只需 groups。

  • 从这儿就能够看出,关于 100 个 group,Index 序列有 100 个,它的容量(capacity)便是 100.也便是说,Index 是不考虑 gap 的。

  • 而关于 Address 序列,它是考虑了 gap 在内的,因而当 gapLen=2 时,Address 序列的 size 是 102,其间有 2 个是 gap。

  • gapStart 这个变量记载了 gap 开端的索引方位(算法确保了整个groups中只需一段接连的gap),即 gapStart=4。留意,gapStart 记载的是 Address,而非 Index。

  • groups 则是实践存在的序列。它对应的实践上是 Address,也便是说,groups 也是包括 gap 在内的。每个 Address 对应 groups 中的 5 个元素,包括 Address 序列中的 gap,也是对应 5 个元素的。因而 groups.size=size*5 而非 capacity*5,别的,有些代码中会见到 physical address 的概念,指的便是 groups 中的单个元素。

  • 终究要说的便是 Anchor 了。Anchor 是依据 Index 得出来的一个值。

    • 当 Anchor 刺进在 gap 之前(含 gapStart 方位)时,它标明的是它到数组最初的间隔。
      • 例如图中的 Anchor1,值为 2.
      • 例如图中的 Anchor2,值为 4.
    • 当 Anchor 刺进在 gap 之后时,它标明的是它到数组末端的间隔。此刻会用负数来符号它是在 gap 后边。(负数仅作符号效果
      • 例如图中的 Anchor3,值为-94.

在你确认现已彻底了解上图后,咱们往下走。

从上图能够看出,Index序列在规划上是不包括gap的,而Address序列 的相关核算是不必管 gap 的。后半句话的意思是,在经过 address 获取 groups 数组中的 group 时,直接用 address * Group_Fields_Size 即可,而不需求再进行 gap 相关的核算。原因便是 Address 这个概念现已把 gap 考虑在内了,Address 序列对应的便是实在存在的 groups 数组,因而经过Address从groups中取数据时,不必再考虑gap的核算问题。

上图的终究列出了三个核算公式。分别是 Index 与 Address 的互转、依据 Index 插 Anchor、取 Anchor 对应的 Index。

提示一点:源码中不同当地的capacity和size存在混用的现象,需求自行区分capacity或许size指什么,图中标注的capacity和size仅仅仅仅为了对应转换公式中的代码。

1.2.1.3 拜访 Group 的字段

好,在 1.2.1.1 节的 Group 字段解说和 1.2.1.2 节的寻址解说之后,咱们总算能够看懂 1.2.1.1 节的那段拜访 Group 字段的代码了。传送

第一个函数便是,经过 address 去groups数组中获取 groupInfo,关于 address 的核算,咱们不考虑 gap,直接乘以 Group_Fields_Size,也便是 5,然后再加上 groupInfo 对应的 offset,也便是 1,即可取到对应的 groupInfo 了。

第二个函数便是,经过 address 去groups数组中判别这个 group 是否有 aux 信息,那么取到对应 groupInfo 的 int 值之后,和第 28 位做与运算,校验是否为 1 即可。

那么这儿仅仅举了两个函数的例子,对其他 group 相关的扩展函数也都相似地看看,咱们就能一步步推知关于 group 结构的更多信息。


有必要提一嘴的是 objectKeyIndex、auxIndex、slotAnchor 这几个函数:

private fun IntArray.objectKeyIndex(address: Int) = (address * Group_Fields_Size).let { slot ->
    this[slot + DataAnchor_Offset] +
        countOneBits(this[slot + GroupInfo_Offset] shr (ObjectKey_Shift + 1))
}

看看它的完结,this[address * Group_Fields_Size]取到了这个 group,但为什么它的 objectKey 的 Index 还要再加上 countOneBits(this[slot + GroupInfo_Offset] shr (ObjectKey_Shift + 1))这一段呢?

countOneBits 函数回来了 0-7 的二进制标明中,“1”位的个数,而 ObjectKey_Shift=29,对照上面的 GroupInfo 构成表看,ObjectKey_Shift + 1 = 30 代表着 node 的方位。假设 groupInfo >> 30,那也就只剩 31 和 30 两位了,而 31 位是 0,那实践上,加上的这一大坨,实践上便是在加上或许存在的 node 导致的索引偏移——node 不存在则是 0,存在则 1。

这整个函数的效果是获取 object key 在另一个数组(slots)中的索引方位。从上面的剖析就能够看出,group 的 objectKey 实践寄存在这个 group 对应的 slots 中的第二个方位(第一个方位是 node,且它们都是可选的)。

而 auxIndex、slotAnchor(data)的索引获取函数也十分相似,他们依次寄存于 slots 的第三个,第四个方位——当然,aux 也是可选的。

这个位运算的规划挺奇妙的,group 的 groupInfo 中,相应的“1”位代表 node、objectKey、aux 是否存在,而因为它们是可选的,“1”位的个数就反映了 slots 数组中实在的数据 slot(即 slotAnchor/data slot)开端的方位。

这一部分能够结合下面1.2.1.4节的图来了解。 传送

1.2.1.4 现在能够获得的情报

那么,到现在停止,咱们都是没有看 SlotTable.kt 最初的那段注释的,那一大段注释,初度看起来或许会十分不知所云,可是,经过咱们之前的剖析,咱们现已掌握了一些信息,现在咱们再回过头去看它。

下面的内容并不是直接对注释的翻译,而是带上了我的了解,能够结合底下的图来辅佐了解。

  • Address
    • Group在 groups 数组中的索引,关于 Address 的核算不需求再处理 gap,因为它自身现已包括 gap 了。
  • Anchor
    • 它是依据Index的包装值,只不过换了一个称号:锚点——它的值不会随着 groups 或许 slots 数组中被刺进或许删去元素而改动,所以叫做锚点。(至于为什么不会变,先不急,后边揭晓
    • 假设锚点的方位在 gap 之前,则它的值是正数,反之是负数。
    • 假设锚点值为负数,则它记载了从数组末端到它的间隔。
    • 假设 slots 或许 groups 数组有新增或许删去,这个间隔不会变,但它能自动反映出删去和刺进操作。(只需比照Address序列和Index序列就能够发现刺进/删去
    • 锚点值仅有会产生改变的状况是:gap 的移动导致了GroupSlotAddress移动时。
    • 锚点这个术语并不仅仅用于 Anchor 这个类,例如在Group的字段中,咱们之前现已见过了 parent Anchor、data Anchor 等术语,它们也是相同的,只不过它们以直接的 int 寄存于Group的字段中,而非 Anchor 类(其实Anchor类便是对Index这个int简略封装了一下)。
  • Aux
    • 辅佐数据,它们能够与 node 产生相关,而且独立于Group的 slots 之外——之前咱们的剖析在这段注释中就得到了印证,在 1.2.1.3 节的终究,咱们剖析到 slots 数组中的前几个方位,或许寄存NodeObjectKeyAux等数据,且它们与Data数据一起寄存在 slots 数组中。
    • 运用场景,例如 composer 运用它来记载 CompositionLocal 映射,因为在组启动时,映射是未知的,只需在运用恣意数量的 slots 之后核算映射时才知道。(这段话看不了解也没联系,CompositionLocal不是本文的内容,咱们只评论SlotTable,这儿就知道它有个运用场景就行了
  • Data
    • 之前现已提过了,一个Group的 slots 中,有可选的NodeObjectKeyAux等“辅佐”数据,那么其后的Data便是“正式”数据了。
  • Group的字段
    • 之前也现已细说,这儿就不再详细解说了,因为开支原因,Group 并没有用类去界说,而是直接以多个 int 值的形式界说,包括 Key、GroupInfo、Parent Anchor、Size、Data Anchor 等,而其间 GroupInfo 又包括了NodeObjectKeyAux等是否存在,以及,假设存在的话,它们在 slots 中寄存的方位等信息,此外,GroupInfo 还有 node count 信息。
    • 别的留意,Group 的 Size 是指子 Group 的数量。Size 是不包括自己的。
  • Group
    • 之前现已提及,一个Group便是 groups 中 5 个接连寄存的 int。
    • Group是一个树状结构,能够包括子Group
    • 因为在 slots 数组中,数据是接连寄存的,因而,Group中的信息能够用来描绘怎么来解说 slots。换言之,咱们能够经过 Group 中记载的索引信息,去 slots 中找这个 group 对应的详细数据——在 1.2.1.3 节咱们现已亲自剖析了这些函数。
    • Group有一个 int 类型的 key,还有可选的NodeObjectKeyAux,然后还有 0 个或多个 data slot。
    • Group实践上是一个树结构,它的子Group在 groups 数组中的寄存方位就坐落它自己后边。
    • 这种数据结构有利于对子Group进行线性扫描。
    • 除非经过Group相关的Anchor,不然随机拜访是贵重的。
  • Index
    • Index是 groups 数组或许 slots 数组中用于标识Group或许Slot的逻辑索引。这个索引不会随着 gap 移动而改变(因为自身就现已把 gap 排除在外了)。
    • 假设 gap 在完毕,则GroupSlotIndex值和Address值是相同的。这在 SlotReader 中得到了充分运用。为了 SlotReader 的简略性和功率,gap 总是移动到终究,导致 SlotReader 中的Index值和Address值相同。
    • 代码中的一切触及 array index 的字段,都是指的Index值,而非Address值,除非它们命名上显式以 Address 完毕。
    • 露出出去的 API 说到的索引都是指Index值,而非Address值。Address值是 SlotTable.kt 内部的。
  • Key
    • 用以仅有标识一个 Group,是一个 int 值,由 Compose 编译器产生,startGroup 函数传入。
  • Node
    • ObjectKeyAux等相似,它也是属于Group的一个辅佐数据,独立于Group的 data slot。
    • 运用场景,例如,当运用 UIEmitter 发射时,LayoutNode 会被存在 node slot 中。(相同,这段话看不了解也没联系,UIEmitter不是本文的内容,咱们只评论SlotTable,这儿就知道它有个运用场景就行了
  • ObjectKey
    • 除 int 类型的 Key 以外,每个Group还有一个辅佐的 Object(Any)类型的Key
    • 运用场景,例如,调用 key()这个@Composable 函数,会产生一个ObjectKey,能够自行去了解一下 key()这个 Composable 的运用场景。
  • Slot
    • slots 数组中的最小单位,一个便是 slots 数组的一个元素。之前说到的NodeObjectKeyAux等辅佐数据和Group的 data slot 等正式数据,都存在Slot里——前三者各占一个 Slot(假设存在的话),后者占 0 个或多个Slot

OK,总算完毕了,上面便是 SlotTable 中的绝大部分概念了,我画了一张图,总结一下。

Jetpack Compose探索(一)——SlotTable

至此,咱们经过读 SlotTable.kt 的代码和注释并剖析的办法,把 SlotTable 的大部分概念和结构给弄了解了,这部分之所以写的很细,花了十分多的篇幅,便是因为想要持续往后看,就有必要先弄懂结构,不然越往后越不知所云。这部分是咱们能持续往后探究的根底。

至于 SlotTable 的结构弄的这么杂乱的原因,无疑是为了功能——例如用 IntArray 线性存储、例如用奇妙的位运算等等。至于规划这样的线性数组为什么就能进步功能,咱们暂时先不评论,持续往后看。

小结一下,1.2.1 节咱们搞清楚了 SlotTable 中有关 group 和寻址的大部分内容,可是还有一些没有讲清楚的部分,咱们梳理一下,大致有这么几条:

  • gap 机制相关的问题:
    • groups 或许 slots 数组中,刺进、删去、移动等操刁难 gap、Anchor、Index 的影响。怎么了解?怎么完结?
  • GroupInfo 一些字段的意义:
    • GroupInfo 中,有 m 和 cm 两个标志位咱们之前没有细说,分别是被符号和含有一个符号。这是什么意思?它们有什么区别吗?
    • GroupInfo 中,还有一个叫 node count 的东西。按咱们之前的剖析,node 仅占一个 slot,那 node count 又是什么?难不成能有多个 node?
  • 规划如此杂乱的线性结构能够进步功能的原因。

别的,再弥补一个有关 Group Child 的小问题。

Group 中说到,Group 实践上是个树状结构,由子 Group 找父 Group 能够经过 Parent Anchor 定位,可是如同没见到有 Child Anchor 这种东西?那么,在子 Group 数量不固定的状况下,怎么经过父 Group 定位子 Group 的呢?

答案是: 之前咱们提过,Group 的子 Group 仅挨着它自己寄存,而 Group 的字段中有一项便是 Group 的 size,这个字段也就记载了它的子 Group 的数量,而且,每个 Group 的巨细都固定是 5。因而,这个 Group 的子 Group 就悉数都能够定位到了。

此后,咱们约定:

  • 独自说到的 groups 代表整个 IntArray,即一切 groups
  • 独自说到的 slots 代表整个 Array<Any?>,即一切 slots
  • group 代指单个 group
  • group slots/slots 段代表这个 group 对应的一切 slots,包括或许存在的 node slot、object key slot、aux slot 与 data slot
  • data slot(s)代表这个 group 对应的 Data
  • node slot、object key slot、aux slot 代表这个 group 对应的 Node、ObjectKey、Aux 的 Slot

而且提示一下:

  • group 的 DataAnchor 指向的是 group slots 的第 0 个 address,而 data slot 的 address 则要从 DataAnchor 处开端往后偏移或许存在的 node slot、object key slot、aux slot,再往后才是 data slot,不要搞混了。
  • group 的 slot anchor 和 data anchor 不是一回事,slot anchor 指向的是 data slot。

好了,咱们带着上面的疑问,持续往下。

1.2.2 代码结构

咱们现在清楚了 SlotTable 的结构,还有那么多问题等着咱们答复,那下一步该怎么持续往下探究呢?不要慌,不要乱,理清思绪,想想现在咱们能干什么?

咱们能够去看 SlotTable 类的代码了。

后文或许以table作为slotTable方针的简称。

1.2.2.1 特点

先来看看 SlotTable 类中的字段。

internal class SlotTable : CompositionData, Iterable<CompositionGroup> {
    //便是咱们之前说到的groups数组,寄存了SlotTable中的一切group
    var groups = IntArray(0)
    //groups数组中,group的数量
    var groupsSize = 0
    //便是咱们之前说到的slots数组,一个group能够经过dataAnchor定位到这个group对应的group slots
    var slots = Array<Any?>(0) { null }
    //slots中已运用的slot的数量
    var slotsSize = 0
    //active状况的reader的数量,一个SlotTable能有多个reader
    var readers = 0
    //是否存在active的writer,一个SlotTable只能有一个writer
    var writer = false
    //active的anchors
    var anchors: ArrayList<Anchor> = arrayListOf()
    //SlotTable是否为空
    override val isEmpty get() = groupsSize == 0

这些字段都是咱们认识的,那就直接持续往后走了。

1.2.2.2 办法

SlotReader/Writer 相关办法

从之前屡次说到 SlotReader 和 SlotWriter,咱们能猜到,对 SlotTable 的读写是经过 SlotReader 和 SlotWriter 进行的。因为需求约束“一起只能有一个 SlotWriter 写这个 SlotTable”以及“读与写不能一起产生”这两个条件,规划者把创立 SlotReader/Writer 的代码操控在 SlotTable 内部,让这个 SlotTable 实例来操控谁能读写自己。这样的规划也是比较奇妙了。

这部分代码比较简略,就不解说代码了。总归呢,外部能够经过下面这四个办法来取到 SlotReader/SlotWriter 实例,进而拜访 SlotTable。

  • slotTable.read((SlotReader)->T)
  • slotTable.write((SlotWriter)->T)
  • slotTable.openReader()
  • slotTable.openWriter()

有关 SlotReader/Writer 的办法,还有 close()办法。在 SlotWriter 调用 table.close()之前,table 是无效的——因为正在写数据时当然不能一起读,当写完数据后,调用 table.close()之后,才干读。

Anchor 相关办法

SlotTable 类中有几个关于 Anchor 的办法,在 1.2.2.1 节的字段中,咱们看到了 table 有个特点 anchors,它是个 Anchor 类型的 ArrayList,虽然咱们现在还不知道这个 anchors 的详细用途,但不妨碍咱们先来看看有关它的操作办法。

看一个就够了。

fun anchor(index: Int): Anchor {
    runtimeCheck(!writer) { "use active SlotWriter to create an anchor location instead " }
    require(index in 0 until groupsSize) { "Parameter index is out of range" }
    return anchors.getOrAdd(index, groupsSize) {
        Anchor(index)
    }
}

这个办法用于获取指定 index 处的 anchor(没取到则新放一个 anchor)。其间,anchors.getOrAdd(index, groupSize)的效果是,在 anchors 中查找索引为 index 的 anchor,找到则回来,没找到则在该 index 新放置一个 anchor。ArrayList<Anchor>.getOrAdd 办法中调用了 ArrayList<Anchor>.search 办法,这个 search 办法便是一个手写的二分查找。

还有几个相似的 Anchor 相关的操作办法,比较简略,就不逐个读了。

Group 和 RecomposeScope 相关办法

SlotTable 类办法的终究一部分是关于 Group 和 RecomposeScope 的,在彻底了解 1.2.1 节后,这一部分的完结逻辑也比较简略(可是RecomposeScope自身是什么,咱们暂不评论),就不展开了,这些函数大致有:

  • findEffectiveRecomposeScope(group: Int): RecomposeScopeImpl?
    • 从 group 参数代表的 group 的 group slots 开端,寻觅第一个有用的 RecomposeScope,假设找不到则不断向父级持续寻觅。然后在 invalidate 时,会导致这个 group 重组。
  • invalidateGroup(group: Int): Boolean
    • 相似上面的查找,也是去找最近有用的 RecomposeScope,假设 invalidate 会导致重组,则回来 true,不然回来 false 会导致其他形式的强制重组。

辅佐办法

终究一部分便是一些调试辅佐办法了,不作介绍。


至此,咱们总算把 SlotTable 类读完了,长呼一口气——歇息一下,看一眼现在的进度。

Jetpack Compose探索(一)——SlotTable

现在 SlotTable.kt 里就剩余两个最大的类了(其他的都是一些无关紧要的协助类),也便是咱们之前说到屡次的 SlotReader 和 SlotWriter,一个 400 多行,一个将近 2000 行。咱们之前的许多疑问还没得到答复,看来,重头戏就藏在这两个类了。

那么,先看哪个呢?次序必定不能乱,当然是要先看恐怖的 SlotWriter 了,因为只需知道了数据是怎么写入的,才干知道怎么去读它——倒不如说,一旦弄了解了怎么写入,那该怎么去读天然而然也就都清楚了。

因而接下来咱们带着之前的一切疑问,直奔 SlotWriter。

2 SlotWriter

虽然这个文件有接近 2000 行,可是不要怕,思路有必要明晰。接下来咱们理理思绪,解读整个 SlotWriter 分为三步:

  1. 大致看看它有哪些特点,这是为第 2 步打下根底。
  2. 直奔它的各种根本操作办法(即group和slot的增修正查移动等)。这一部分是为第 3 步打下根底,十分重要,不过,重点别搞错了,读操作办法的源码仅仅为了让咱们更了解整个 SlotWriter 的根本运作,而并非实在要去看完每一行代码。其实,以咱们在第 1 节中打下的根底,啃完一切操作办法的源码并不会有太大问题,可是真没必要。因而这一部分,咱们也只挑几个典型的操作办法去看看,至于剩余的,有需求用届时我会给出这个办法的简要相关阐明。
  3. 尝试触摸 SlotWriter 的核心,也便是外界 Compose 结构经过操作 SlotWriter 来给 SlotTable 写数据的代码(startGroup等),那才是咱们的重点。

把这些悉数都搞定,那么咱们的 SlotWriter 这一节也就完毕了。下面直接开端,咱们先看看 SlotWriter 里面常见的一些特点和概念。

2.1 特点和概念

val table: SlotTable

这个就没什么好说的了,SlotWriter 在结构时就会把要写入的 table 传入。

groups: IntArray = table.groups

groups,咱们的老朋友了,可是 SlotWriter 里的 groups 的注释告诉了咱们更多的信息:

  • 当有新 group 刺进、且会导致 groups 需求扩容时,这个 groups 或许产生改变。
  • 因为 gap 的存在,groups 内有些空间是代表 gap 的,那么有用的、代表 group 的区域怎么散布呢?见下图。(这其实便是 1.2.1.2 节说到过的内容

Jetpack Compose探索(一)——SlotTable

这个图其实画的有一点点小问题,懒得去调整了,gap的宽度必定是group宽度的整数倍。

slots: Array<Any?>

slots,与 groups 几乎彻底相同,包括 gap 的散布办法,因而不再赘述,看上图即可。

anchors: ArrayList<Anchor>

一个 anchor 数组,用来记载一些 group index,但详细的意义咱们暂不知晓。(其实是外界操作SlotWriter时留下的一些定位符号,这篇文章中咱们不会过多介绍anchors详细的用途,咱们更多重视SlotTable和SlotWriter自身。

后文中,为了区分这个anchors和其它anchors,会把这个anchors称为SlotWriter的成员变量anchors或类特点anchors,请留意区分。

groupGapStart: Int

gap 开端的索引。对照 1.2.1.2 节的图的 Address 序列进行了解。传送

groupGapLen: Int

groups 中 gap 的数量。对照 1.2.1.2 节的图的 Address 序列进行了解。

slotsGapStart/slotsGapLen: Int

类比上面 groups 的,相同了解。

slotsGapOwner: Int

这个 gapOwner 的概念会比较简略搞错。首要,slotsGap 的 owner 是指一个 group,而不是一个 slot。而 gapOwner 是把 gap 自身也当作了一个 group 来看,因而 gapOwner 的实践值要在 gap 前的终究一个 group 上+1,换句话说,owner 的取值规模是[1,size],而非[0,size-1]。

Jetpack Compose探索(一)——SlotTable

例如,在这张暗示图中,slots 中的 gap 跟在 group1 的 slots 之后,gap 属于 group1 后的一个“group”,那么 owner 便是 group1 的 index 再+1,在这张图中,也就指向 group2 了。(简而言之便是owner要多+1

val startStack/endStack: IntStack

显示启动的 group 会被记载在 startStack 里,这个 stack 是一个 IntStack 类型,便是一个全为整数的栈结构,后进先出。endStack 与之对应。

这俩玩意都是与 startGroup、endGroup 等重要操作有关的,咱们先不急着了解。

var currentGroup: Int

便是当时即将要进行 start/skip 等操作的 group。

val isGroupEnd get() = currentGroup == currentGroupEnd

假设说 currentGroup 现已在 group 的完毕了(Group是树状结构,能够包括子Group的),意味着拜访完了,要去调用 endGroup。

暂时先介绍这些概念。

2.2 根本操作办法

一些太细碎辅佐性的办法(例如,依据parentAnchor获取parent group、依据index获取groupKey之类的)咱们就不过多介绍了,只需第 1 节看懂了,这些都没问题。

这一节咱们首要看看对 groups 和 slots 的根本操作办法,例如增删、移动、扩容等等。

2.2.1 slot 的操作办法

首要看看 slot 的。

2.2.1.1 moveSlotGapTo

moveSlotGapTo(index: Int, group: Int)

咱们看的第一个办法,moveSlotGapTo,效果是 slots 中的 gap 移动到 group 对应的 slots 段中的 index 这个方位,以便于能够为 group 增加一些新的 slots。

这儿提一嘴,参数中的index和group应该是有相关的,仅仅函数中没有再去校验(反正是内部源码,也不是露出给一般开发者调用的,多校验一下就多一点开支)。这儿说的有相关的意思是,参数index应该是在group对应的slots段之内的,后边的几个函数也都是这样。

这整个函数的完结首要分了两步,我画了两张暗示图。

第一步

Jetpack Compose探索(一)——SlotTable

第一步,把 slots 中的 gap 从 gapStart 方位移动到 index 方位。

图中的蓝色圆圈代表的是需求移动的数据,能够看到,当 gap 移动时,实践上受影响的只需蓝色圆圈的数据,而再往前或许往后,是不会有任何影响的。说白了,这个操作便是把蓝色圈圈的数据和 gap 做交流。

图中的横线标明 slots,横线上标明 before,也便是移动前,横线下则是 after,也便是移动后。

咱们看景象 1,当 index<gapStart 时,直接用数组的 copy 办法移动蓝色部分的数据:方针数组 destination 仍是自己,而 destinationOffset 则是 index+gapLen,要 copy 的蓝色部分的起始方位 startIndex 是 index,完毕方位是 gapStart。copy 后,蓝色圈就到后边去了,然后咱们再把 index~index+gapLen 段置 null,即形成了新的 gap,完结了换位。

景象 2 以及当 gap 不重合时的景象,就不赘述了,能够自己看看。

第二步

Jetpack Compose探索(一)——SlotTable

那么 moveSlotGapTo 办法的第二步,则是更新 dataAnchor。因为咱们移动了 gap,而前面现已说过,Anchor 是与 gap 方位有关的,因而要更新受影响的 anchors。

图中代码只贴了 newSlotsGapOwner>slotsGapOwner 的状况,另一种状况相似,就不贴代码了。

能够看到,受 gap 移动影响的 anchor 是 group2 和 group3 的 data anchor,因而咱们要更新它们。详细的代码逻辑,对照图慢慢看就能看懂,不再赘述了。

整个 moveSlotGapTo 就剖析到这儿。只需当有了 gap 咱们才干履行刺进等操作,咱们接下来就看看刺进。

2.2.1.2 insertSlots

insertSlots(size: Int, group: Int)

我直接依据源码给出我归纳的进程。

  1. 调用 moveSlotGapTo(currentSlot, group),先把 slots 中的 gap 挪到要刺进 slot 的 group 所属的 slots 段。这儿 currentSlot 也是 group 对应的 slots 段的索引。

  2. 假设当时 gapLen 不足以刺进 size 个 slot,则扩容。

    1. 新的 gap 的巨细为 slots.size * 2、slots.size – gapLen + size、32 三者的最大值。依据这个巨细创立一个 newData 数组。
    2. 然后依旧是调用 array.copyInto,经过两次 copyInto 调用,把数据从旧数组拷贝到新数组,这样 gap 天然就扩大了。

    Jetpack Compose探索(一)——SlotTable

  3. 更新 slots、slotsGapLen、slotsGapStart、currentSlotEnd 等各个受影响的 SlotTable 的特点。

能够看到,insertSlots 的完结仍是比较简略和明晰的。先把 slots 的 gap 移动到要刺进 slots 的 group 对应的 slots 段下,然后假设 gap 长度够,则直接 insertSlots,不然扩容。

2.2.1.3 removeSlots

removeSlots(start: Int, len: Int, group: Int)

关于 group 对应的的 slots 段,从 start 开端移除 len 个 slots,首要进程如下。

  1. 调用 moveSlotGapTo(start + len, group),把 gap 移动到要删去的部分的末端。
  2. 直接把 start~start+len 段置 null,并修正 slotsGapStart 和 slotGapLen,就把 gap 扩容了,也就相当于 remove 了 slots。

这样,removeSlots 确保了 slots 被删去(实则为置 null 了),然后把 remove 掉的部分一起算入 gap 段内,一起,这样也确保了 slots 中只需仅有一段接连的 gap,以便后续进行其它操作。

2.2.2 group 的操作办法

slot 的操作办法暂时先看上面那些,接下来看看 group 的,大多与 slot 的操作办法相似。

2.2.2.1 moveGroupGapTo

moveGroupGapTo(index: Int)

groups 数组与 slots 数组相同,也有一段 gap,天然也有 moveGap 办法,那么这儿的参数 index 便是要移动到的索引,能够对照 1.2.1.2 寻址一节的图来了解。 传送

首要进程如下。

  1. 更新 anchors。这儿的 anchors 是指 2.1 节说到的成员变量 anchors,gap 的移动导致一切记载的 anchors 需求更新。更新的逻辑对咱们来说现已很清楚了,因为咱们现已知道了 anchor 的核算规矩,那么比照新旧 gap 找出受影响的那些 anchors,然后按照 anchor 核算规矩从头核算值就行。
  2. 调用数组的 copyInto 移动数据。这一步与 moveSlotGapTo 相似,能够参照 moveSlotGapTo 的图了解。需求留意的是,groups 中咱们要按照 Address 序列来操作,也便是把 Address 序列转换为实在物理地址,说白了也便是*5,这一点也在寻址一节中讲过了,而 moveSlotGapTo 不需求,因为每个 group 的 slots 段长度不固定,自身便是依据 group 区分和定位的。假设前面了解透了,这儿的地址转换是十分好了解的。
  3. gap 移动或许导致 parent anchors 也需求更新,因而,终究更新受影响的 parent anchors。
  4. 终究更新符号,groupGapStart 更新为 index。
2.2.2.2 insertGroups

insertGroups(size: Int)

刺进 size 个 group 到 currentGroup 之前,这些刺进的 group 和 currentGroup 都有同一个 parent。首要进程如下。

  1. 调用 moveGroupGapTo(currentGroup)。能够发现,不管是 insertGroups 仍是 insertSlots,都是经过先把 gap 移到想要 insert 的方位,再直接往 gap 里刺进,来完结的。
  2. 假设 gapLen 不够,则扩容。这儿与 insertSlots 也几乎相同,不再赘述。
  3. 更改 currentGroupEnd、groupGapStart、groupGapLen、slotsGapOwner 等成员变量,以标明现已刺进了若干新 group。
  4. 设置 data anchors。关于新刺进的 group,它们的 data anchor 和 currentGroup 相同。
2.2.2.3 removeGroups

removeGroups(start: Int, len: Int): Boolean

group 的 remove 操作与 slot 的也是相似的。这儿先把 gap 直接移动到 start 方位,然后把 start~start+len 段都符号为 gap,就完结了所谓的 remove 操作,一起也确保了 gap 只需一段且接连。

除此以外,group 的 remove 操作还需求删去或许的 anchors,这个 anchors 是指成员变量 anchors,removeGroups 函数的回来值也是标明 anchors 是否都被移除了。

别的,slotsGapOwner 和 currentGroupEnd 等符号成员变量也要进行相应更新。

终究,假设 parent 含有 groupMark 的话,还要更新 parent 的 mark,可是咱们现在还没介绍 mark 是啥,所以先不管它。

乍一看 remove 的咱们剖析到这儿也就完毕了,可是,这儿有一个问题。咱们把若干 group 移除了,可是如同没同步移除它们对应的 slots 呀——其实是有的,只不过移除 slots 的代码是在removeGroup函数内,不是在这儿。

2.3 核心办法

到这儿,咱们对 group 和 slot 的根本操作办法都有了必定的了解。接下来便是重头戏了。咱们要来看看,Compose 结构怎么操作 SlotWriter 来改动 SlotTable 的。

在持续之前,我再次提示一下,因为代码太多太庞大,咱们的思路和当时使命必定要明晰,咱们要清楚这一次咱们探究的鸿沟何在,千万不能乱,不然很简略迷失。

整个第2节(即SlotWriter一节),咱们触及到外界(即Composer、Recomposer等人物)的当地仅仅只需它们操作SlotWriter的当地。换言之,咱们现在的重心仍是放在SlotWriter内部的。咱们现在的使命是看SlotWriter的startGroup、endGroup、moveGroup等操作,究竟咱们整篇文章是偏重SlotTable自身,而不是偏重Composer等外界的组合重组逻辑的。因而,假设你要问例如Composer中的start、end是怎么协作的,它们又和咱们写的@Composable有什么联系、亦或许Recomposer相关的问题,那么,这篇文章里是不会过多提及的,仅仅只会大致介绍一点相关的部分,相当于提早铺垫一下,浅尝辄止。

Jetpack Compose的整套系统过于庞大,从Compose编译器,到Composer、Composition、Recomposer、SlotTable,再到与Android接轨的AndroidComposeView等等,必定不是一口气能啃下来的。所以我在文章中间的这儿再次提示,咱们要清楚这次探究的鸿沟,有必要时间清楚这部分咱们是在了解什么内容,哪些内容能够现在仅做大致了解等等。

好了,咱们持续。

2.3.1 从 Composer 看起

咱们之前现已知道,Compose 编译器会把咱们写的@Composable 函数包装成各式各样的 group。从 Composer.kt 中,咱们能够知道有许多种类型的 startGroup,例如:

  • startReplaceableGroup/可替换组 – 指不能在同级组之间移动,但能够被移除或刺进的组。编译器会在@Composable 函数中的条件逻辑分支(例如 if 表达式、when 表达式、提早 return 和 null 合并运算符等)周围刺进这些组。

  • startMoveableGroup/可移动组 – 指除了被移除或刺进之外,还能够在其同级之间移动或从头排序并坚持 SlotTable 状况的组。可移动组比较贵重,仅会由编译器在 key 函数调用后刺进(即用于长列表等)。

  • startRestartGroup/重启组 – 用于记载一个@Composable 函数的组,这个被记载的函数可按需被部分再次调用。

  • startNode – 在这儿,咱们总算要解说之前说到过屡次的 Node 了。在 Composer.kt 文件的完毕,有一个类叫 GroupKind,即 Group 的种类,从这个类咱们能知道,Group 一共有三种类型:

    • Group – 普通 Group
    • Node – Node Group
    • ReusableNode – 可重用的 Node Group

    Node 类型的组实践上盯梢记载了一段代码,这段代码用于创立或更新 Node 节点。而这儿的 Node 节点指的是,由 Composition 所标志的树的节点。咱们知道,Composition 虽然标志着一个树结构,但并不是 Compose 用于实践渲染的树,这儿的 Node 节点指的便是,生成的实践渲染的树的 Node 节点。而咱们的 Node Group,存的是一段可履行代码。这段代码是用于创立或更新 Node 节点的。

    咱们之前看到的 Group 的字段中,有关 Node 的部分,在这儿就得到解说了。假设 start 了一个 Node Group,则 Group 的字段中,会有相应的记载。

    到这儿了,咱们有必要意识到一件事,或许说有一种观念,即,函数也是一个方针。

    Kotlin和Compose的规划中,大篇幅能看到这样的思想。函数便是一段可履行的代码,再说白点,便是一系列操作。lambda的概念与之十分相似。而函数的界说和完结便是界说了这些操作,而函数的调用才是去履行这些操作。因而咱们当然能够把这些操作先仅仅界说出来,而不去履行它们,而是以方针的形式,把它们存下来。

    这便是Group中经常能看到的,一些Group的Data实践上是一个Lambda,或许说一个函数方针。相同,在Composer的代码中,也有很多相似的概念,例如Change等。它们都是先界说好操作,然后把它当方针存起来,需求时再去履行。

除了上面说到的这些以外,当然还有一些 startXXXGroup 的办法,例如 startDefaults、startRoot、startReusableNode、startReusableGroup 等(还有一些别的办法,例如 buildContext 办法,也会调用 startGroup),详细的咱们不再去细究了,再往下看就有点离题了。咱们这篇文章仅仅研究 SlotTable,不研究 Composer。

回到正题,咱们持续往下跟。咱们刚刚看到 Composer 中各种 startXXGroup,不管是谁,它们终究都调用了一个办法——start,那么,这个 start 便是重中之重了,咱们来看一看。

start(key: Int, objectKey: Any?, kind: GroupKind, data: Any?)

不同的 startXXGroup 办法,对 start 传入了不同的参数,这些参数咱们都不陌生了——key、objectKey、kind、data。

Jetpack Compose探索(一)——SlotTable

能够看到,只需 startNode 和 startReusableNode 传入的 GroupKind 不是 GroupKind.Group。总归,咱们的各类数据在这儿就要被打包成 Group 存进 SlotTable 了。咱们详细来看看这个 start 函数。

这个 start 函数很长,但整体头绪很明晰。咱们暂时只重视它与 SlotWriter 接轨的部分——当 Composer 处于 inserting 状况时,start 函数中会直接去操作 SlotWriter 来直接新增 Group。

若Composition当时正要刺进节点到树中,则处于inserting。第一次组合时,一直处于inserting,重组时,当有新节点要被刺进到树中时,处于inserting。

在刺进 Group 的前后,会成对调用 writer 的 beginInsert 与 endInsert 办法。

(以下简称begin)因为Group是树状结构,因而begin是能够屡次调用的。当begin被调用,而end尚未被调用时,假设这时再去调用begin,就意味着产生了嵌套,即,再次begin的是当时Group的子Group。

那么,在当时(也便是最外层)的Group要begin时,咱们记载一下currentGroupEnd,然后,当时(最外层)的Group要end时,经过比照之前begin时记载的currentGroupEnd,咱们就能知道这期间它的子Group是否有刺进或删去操作——因为咱们之前说过,子Group是紧接着当时Group摆放的,currentGroupEnd的规模是包括了一切子Group的。

持续 start 的流程,刚刚说到假设是 inserting,就操作 writer 进行新增 Group,调用的代码便是下面三行。

when {
    isNode -> writer.startNode(key, Composer.Empty)
    data != null -> writer.startData(key, objectKey ?: Composer.Empty, data)
    else -> writer.startGroup(key, objectKey ?: Composer.Empty)
}

那么,不管 startNode/Data/Group,它们都相当于一堆重载办法,终究都调用到 SlotWriter 的 startGroup 办法。接下来,咱们就进入这个办法持续跟进。

2.3.2 start/endGroup

startGroup(key: Int, objectKey: Any?, isNode: Boolean, aux: Any?)

startGroup 中,依据是否处于 inserting 分为两种状况。

假设要新增节点时,就会一直处于 inserting 形式,咱们这一节先来看从零开端新增节点的进程。

  1. 首要调用之前 2.2.2.2 节的 insertGroups(size=1) 函数,把 gap 移动到 currentGroup 前,然后直接往 gap 中新增一个 group,并为它设置好 group 的根本信息。(例如key、objectKey、node、aux、parentAnchor、dataAnchor等。然后,假设有node、aux、objectKey等辅佐字段,还要按1.2.1.4节图中介绍的那样,设置好相应的dataSlots信息。)传送
  2. 因为是新增一个 Group,那么,新增的 Group 必定是树上的叶子节点,即没有子 Group。在新增之后,咱们会暂时把 parent 设置为新增的这个 Group 自己。(此刻,parent变量的意义是:假设要新增新的节点,新的节点的父节点便是parent。
  3. 终究,把 currentGroup 设置为 parent+1。(此刻,currentGroup变量的意义是,假设要新增新的节点,新节点的方位挂在parent下。
  4. 更新其它变量。(例如currentSlotEnd、currentGroupEnd等。

单单这么看,其实会感觉构建进程云里雾里的。没错,别忘了 endGroup。咱们之前提过,endGroup 和 startGroup 是成对的。有一次 startGroup 调用就要有一次 endGroup 调用。那么咱们接下来就一起看看 endGroup。

当处于 inserting 时,endGroup 只干了这么两件事:

  1. 更新 groupSize、nodeCount 等值——哪个 group 的?当然是指更新与这个 endGroup 对应的 startGroup 中新增的那个 group 的 groupSize 和 nodeCount。(从这儿也能看出来,end和start是一对,当调用完end,才算实在完结一个Group的新增。
  2. currentGroup 坚持不变,而 parent 变量更新为 parent 的父 Group。

好了,整个从零开端构建树的算法便是这样。假设你还没反响过来,那么我画了一张暗示图,请看。

Jetpack Compose探索(一)——SlotTable

图中,黄色代表 currentGroup,蓝色代表 parent,而 currentGroup 在 inserting 时代表即将新增的节点,因而以虚线标明。在底下,我画出了每次操作后 groups 的 Index 序列存储的状况。能够看出,这样办法的构建,也恰好契合咱们在1.2.1.4 节中说到的,子 Group 在存储上就排在它自己后边。传送

能够大约猜出,Composer 会去按想要结构的结构,在 inserting 状况下,以特定的次序去调用 startGroup 和 endGroup 办法,能够说,start/endGroup 的调用次序和次数决议了一棵树最初的样子。比方,inserting 时接连调用 startGroup 将导致树将按深度优先的办法成长。

别的提一嘴,startGroup 是 Composer 仅有能刺进新 Group 的当地——假设当时是 inserting 状况,则会新 Group。(还有一个叫bashGroup的操作也能在SlotTable中刺进一个Group,而且是直接刺进parent,但那是Recomposer触发的行为,且用途意义也和单纯的新增构建不相同,咱们暂时不评论。

需求留意的是,inserting 新增时,如同并没有一起传入新增的 Group 的 data slots,顶多只只填充了一些新增的 Group 的辅佐信息 slot,那么 data slots 在什么时分设置是一个问题,不过咱们之后再说。此外,非 inserting 时调用 startGroup 的逻辑,咱们也往后稍稍。

2.3.3 moveGroup

看完了新增,咱们再看看移动 Group。

在 Composer 的 start 函数里,除了或许新增节点外,还有一种或许,便是在兄弟节点之间进行移动。这儿的移动仅指,从后往前移。至于 Composer 什么时分会进行这样的操作,以及为什么只能从后往前移,咱们这篇文章不会提及。咱们的偏重点在 SlotWriter 移动 Group 的详细操作。

moveGroup(offset: Int)

moveGroup 办法会将 currentGroup 后的第 offset 个 group 移动到 currentGroup 前,这儿的 offset 个是指与 currentGroup 同级节点的往后 offset 个,而非 groups 数组的往后 offset 个。

咱们首要定位到方针的 Group 节点 groupToMove,然后核算出移动的长度 moveLen=groupToMove.groupSize,再核算出需求一起移动的 groupToMove 的 slots 的索引 dataStart 和 dataEnd。

移动时,不能处于 inserting 形式。移动的整体流程是:

  • 在新方位刺进空位
  • 把待移动的 group 和对应 slots 拷贝到空位
  • 删去原方位的原数据

可是,详细流程并没有这么简略,因为咱们还要考虑空隙的移动导致的各类 anchors 有必要正确更新值。那么,详细的操作次序就十分重要了,进程如下。

  1. 关于 slots,在方针新方位刺进空位(有必要是第一步)。

    insertSlots(moveDataLen, max(currentGroup - 1, 0))

  2. 关于 groups,在方针新方位刺进空位。

    insertGroups(moveLen)

  3. 把要移动的 groups 拷贝到新方位。

    groups.copyInto(
        destination = groups,
        destinationOffset = currentAddress * Group_Fields_Size,
        startIndex = moveLocationOffset,
        endIndex = moveLocationOffset + moveLen * Group_Fields_Size
    )
    
  4. 把要移动的 slots 拷贝到新方位。

        slots.copyInto(
            destination = slots,
            destinationOffset = currentSlot,
            startIndex = dataIndexToDataAddress(dataStart + moveDataLen),
            endIndex = dataIndexToDataAddress(dataEnd + moveDataLen)
        )
    
  5. 更新受影响 group 的 dataAnchor。

        for (group in current until current + moveLen) {
            groups.updateDataIndex(groupAddress, newAnchor)
        }
    
  6. 更新受影响的成员变量 anchors 中的 anchor。

    moveAnchors(groupToMove + moveLen, current, moveLen)

  7. 删掉之前的旧 groups。

    removeGroups(groupToMove + moveLen, moveLen)

  8. 更新受影响 group 的 parentAnchor。

    fixParentAnchorsFor(parent, currentGroupEnd, current)

  9. 删掉之前的旧 slots(有必要是终究一步)。

    removeSlots(dataStart + moveDataLen, moveDataLen, groupToMove + moveLen - 1)

以上便是悉数的进程和次序了,有些进程之间的次序不能乱,比方 7 有必要在 9 前面,因为删 slots 时需求移动 gap,这个操作依靠于相应 group,有必要先确保相应 group 不再是旧值才行。总归,按以上次序,咱们能正确完结把方针 group 从后往前移的操作。

上面每个进程的操作,大部分都是咱们剖析过的函数,还有像 moveAnchors、fixAnchorsFor 这样的函数咱们没有剖析,它们实践上比较简略,自己直接看源码是没有太大困难的,因而就不再占篇幅去讲了。

别的,外界 Composer 除了新增和移动 group,还能够 removeGroup,不过 removeGroup 的完结也比较简略,因而也不再去精读了。

此外,与移动有关的还有几个函数,moveTo、moveFrom,moveIntoGroupFrom 以及它们依靠的 SlotWriter.moveGroup 函数。这几个函数,细节咱们就不看了,大体完结逻辑也是先开辟空位,然后拷贝,然后删去旧数据,而且更新该更新的 anchors 和变量等等。这些函数是用于在两个 SlotWriter 之间移动 group 的,如有必要,咱们在之后的文章的相应部分再去剖析它们。

2.4 现在能够获得的情报

到此停止,这个 2000 行的 SlotWriter,咱们根本上就看完了,长舒一口气~

咱们小结一下,依据第 1 节咱们对 group、slot、slotTable 结构的了解,在本节中,咱们先大致剖析了 SlotWriter 类的特点和根本操作办法,然后详细阐明晰 SlotWriter 的根本操作办法,然后从 Composer 切入,详细剖析了 SlotWriter 的核心操作办法。

咱们现已揭开了整个庞大的 Compose 结构的冰山一角了,可喜可贺。

那么相同地,咱们有必要理清思绪,看看现在还有哪些咱们遗留的,说要以后再看的问题。

  • anchors 相关
    • 咱们现已知道成员变量 anchors 是 Composer 用来打符号的,那么它究竟起了一个什么效果?
  • node 相关
    • 咱们在剖析的进程中,实践上是疏忽了 node 相关的内容的。虽然在 2.3.1 节中略微解说了一下 node,但咱们仍然对它的详细概念模糊不清,例如:
      • nodeCount 是有什么意义?详细有什么用?怎么核算?
      • nodeCountStack 的用途?
  • mark 相关
    • 还记不记得咱们在第 1 节说到了一个 mark,这个 mark 也被咱们全程疏忽了。那么它又是干什么的呢?
  • 操作办法相关
    • 2.3.2 节中评论 start/endGroup 时,只看了 inserting 下的状况,那么非 inserting 时呢?
    • 还有一些操作,例如 bashGroup、seek、skipGroup,它们有何效果?
    • moveFrom/To 等这类触及两个 SlotWriter 的操作,有何效果?
    • startGroup 新增 group 时,并没有设置 data slots,那 data slots 是什么时分设置的?
  • Composer 相关
    • 2.3.2 节说到,Group 树的结构是由 Composer 调用 start/end 的次数和次序决议的,那么 Composer 怎么决议这些的?start/end 的调用是怎么安排的?
    • 除了新增以外,删去、移动等等操作又产生在何时?
    • 什么操作会触及两个 SlotWriter?
    • 除了 SlotWriter 外,别忘了还有个 SlotReader。它们是怎么协同运作的?

咱们看懂了 SlotWriter 现已是很不简略了,但随之而来的是一个更庞大、更杂乱的人物等着咱们——Composer。要想实在搞懂一切疑问,就不得不深化 Composer 去探究了——这个 Composer.kt 有 4000 多行。

好了,别担心。Composer 的探究咱们不会在这篇文章进行,那是下一篇的内容。这一篇文章屡次说到 Composer,仅仅为了搞点铺垫,让咱们先跟它打个照面。咱们的重心仍是在 SlotTable 和 SlotWriter/Reader 内部的。

到这儿,SlotWriter 的咱们现在能剖析的一切内容就差不多要剖析完了。终究,作为收尾,咱们来看看 SlotWriter 的 close 办法。

2.4.1 close

关于 close(),我想说的是,假设你查找一下 Composer 中 close 函数的调用途,就会发现一个十分有意思的工作。例如,在 Composer 的成员变量界说处,或许创立一个新的用于写操作的 SlotTable 之处,它的代码是这样的。

//成员变量界说
private var writer: SlotWriter = insertTable.openWriter().also { it.close() }
//创立新的用于刺进的slotTable
private fun createFreshInsertTable() {
    runtimeCheck(writer.closed)
    insertTable = SlotTable()
    writer = insertTable.openWriter().also { it.close() }
}

啊?没搞错吧,一创立就把它关了。

那么实践上,close 函数中做了两件事。

  • 把 gap 移动到 SlotTable 的终究。
  • 把 writer 中对 groups、slots、groupsSize、slotsSize、anchors 等特点的更新保存到 slotTable 中。(比方当writer产生扩容时,groups、slots会改变,且其他操作时size、anchors都或许改变

哦,那么上面的操作就能够了解了。刚对这个 slotTable 创立一个 writer 就 close 的意图实践上是想做第一件事,便是把 gap 移动到终究,这样 groups 的 Index 和 Address 序列就没有区别了,便利后续操作。

担任收尾的 close 就剖析到这儿。

3 SlotReader

现在能够说,咱们现已把最难的 SlotWriter 部分啃完了,在这个进程中,咱们对 SlotTable 存储数据的办法又有了更深的了解,而且,在 Reader 中,gap 总是在终究,groups 的 Index 和 Address 序列相同,也就没有 Index 和 Address 的概念之分了,这样,SlotReader 读起来会轻松许多。那么,这一节,就来看看 SlotReader 吧。

什么?你说 SlotReader 现已看完了?怎么或许!

别怕,我告诉你,这是真的,SlotReader 确完结已看完了,在第 1 节和第 2 节里,咱们几乎现已剖析清楚了一切内容。这下你再去看一眼 SlotReader 的代码,便是光秃秃地一览无余,几乎一切内容了解起来都十分简略。

就比方咱们在 SlotWriter 中有各式各样的符号,可是 Reader 中就只需寥寥几个,比方 currentGroup/Slot、currentEnd、parent 等等,它们就相当于一些游标,记载一下当时读到哪个 Group 了罢了。仍是那句话,咱们现已在写的时分弄清楚了一切结构,读还不会读吗?

因而,接下来,我只再弥补几个关于 SlotReader 的小点,咱们就算读完 SlotReader 了。

1、解说一下 SlotReader 中前几个成员变量的注释。

//A copy of the SlotTable.groups array to avoid having to indirect through table.
private val groups: IntArray = table.groups

比方这个 groups 的注释,他说创立了一个 SlotTable.groups 数组的副本,避免不得不间接经过 table 来拜访。

我一开端还认为他想创立这个 groups 数组的副本,但好像 reader 自身也仅仅担任读啊,创立副本干啥?后来才反响过来。

他的意思是说,他界说了一个叫 groups 的变量,直接给它赋值为 tables.group,这样以后想在 SlotReader 内拜访 groups,就不必每次多写一个“table.”。他说的 copy 是这个意思。

2. 简略解说一下几个成员变量。

  • currentGroup – 一个游标,标明 startGroup 或许 skipGroup 中,即将要被操作的组。
  • parent – currentGroup 的父 group,它是 startGroup 启动的上一个 group。
  • currentEnd – parent 的末端。
  • emptyCount – 记载 beginEmpty 调用的次数。
  • currentSlot – 一个游标,代表 parent 的当时 slot,只需还没有移动到 currentSlotEnd,调用 next 办法时,它就会移动到下一个 slot 的方位。
  • currentSlotEnd – parent 的 slots 的最末端。

留意,这儿的 currentSlot 和 currentSlotEnd 与 SlotWriter 中的有所不同,Writer 中记载的索引游标是整个 slots 数组的游标,而 Reader 这儿因为仅仅读,currentSlot 只需记载当时 parent 的游标即可。换句话说,有用的 currentSlotEnd 取值规模是 0 到if (current >= groupsSize - 1) slotsSize else groups.dataAnchor(current + 1)。而有用的 currentSlot 取值规模是[0, currentSlotEnd)。乃至,在 Reader 的 startGroup 中,currentSlot 的值会直接定到 slotAnchor,而非 dataAnchor,这也是为了便利读取。

要解说的便是这些了。其它的以咱们对 SlotTable 的了解,都能轻松看懂。

3、emptyCount/beginEmpty/endEmpty

这个 emptyCount 便是一个记载变量。每逢处于 inserting 形式下,Composer 调用 start 时,这个变量就会+1,对应地,调用 end 时-1。它也相似之前 SlotWriter 中说到的,是能够嵌套调用的。

这个变量用于做记载,以确保 next 和 skip 等操作的正确性。例如,在 next 办法中咱们会读取下一个 slot,可是,当 inserting 时,天然是不能读取的,因而,当 inserting 时,emptyCount 大于 0,读到的永久便是 Empty。相似地,在 skip 中也有相似的操控,skip 时不能处于 inserting。除此外,在其它的一些函数中也有相似操控,例如 Reader 的 startGroup 若处于 inserting 则不会进行。

fun next(): Any? {
    if (emptyCount > 0 || currentSlot >= currentSlotEnd) return Composer.Empty
    return slots[currentSlot++]
}

那么,在 SlotReader 一节的终究,咱们扫个尾,简略看看 SlotReader 中的 startGroup、endGroup、reposition 等读取 SlotTable 的函数。

3.1 拜访 SlotTable

Reader 去读 SlotTable 的办法也是相似 Writer 的,经过更改 currentGroup、parent 等游标的定位来拜访,但在细节上又与 Writer 有所不同。

在 Reader 中,能够经过 start/endGroup、skip、reposition 等操作操控游标。就如刚刚所说,这几个函数的有用调用都需求非 inserting。它们的完结代码不难,比 Writer 的短多了,相同地,我也给了一张暗示图。

Jetpack Compose探索(一)——SlotTable

这个图简略展现了各个函数的效果以及拜访的流程。咱们能够经过这些函数来对游标进行操控,进而拜访需求拜访的 Group。

实践上的流程或许并没有那么简略,你或许会发现,假设在初始状况,我直接调用startGroup,然后调用reposition(5),然后endGroup,假设只依照咱们现在的了解,这个操作并不会报错,可是,这时分就会得到一个不匹配的currentGroup和parent,这是有问题的。因而,这一现象咱们暂时当作疑问记下,因为仅从现在咱们掌握的信息来看,并没法解说这一点。我猜想实践上是会报错的,或许和emptyCount的runtimeCheck有关,但这要等咱们以后探究了Composer才干知道了。

所以这个图便是简略解说了一下各个函数,可是至于实践上它们是怎么协作的,暂时无从知晓。

4 小结

好了,这篇文章到这儿就总算完毕了。祝贺你,SlotTable.kt,这 3000 多行代码,绝大部分内容现已彻底弄清楚了。

剩余的,除了咱们之前说到的暂时不评论的一些疑问,就只需边角料了——一些无关紧要的辅佐函数、以及一些自己能够轻松看懂的辅佐代码。

现在咱们再来总结归纳一下 SlotTable,首要,就如文章最初所说,SlotTable 便是 Compose 结构储存各类数据的当地。别的,SlotTable.kt 文件里,还有两个大类 SlotWriter 和 Reader,它们提供了对 SlotTable 的结构和拜访才能。

因为我并没有找到其它关于 SlotTable 的详细剖析(但仍是引荐看一下fun佬的这篇文章,协助很大。传送门),因而,这篇文章是我硬读这几千行代码,然后考虑总结写出来的(文中的那些各式各样的流程图,也是我自己画的,不是网图,假设看不清,点击这儿看高清大图)——我想说的是,或许会有错误或许描绘不清的当地,欢迎纠正和评论。

至于为什么会去读这个源码呢?纯粹是猎奇和爱好。它的确太底层了,以至于读完也对 Compose 的运用没啥协助,可是,假设想持续探究 Compose 的原理,SlotTable 便是有必要要搞清楚的一个东西。

那么,接下来该持续往哪里探究呢?咱们现在在文章中留下的绝大部分疑问(包括第 0 节提出的好几个疑问),现在看来都与 Composer.kt 有关。所以持续探究 Compose 结构的下一站,就确认为 Composer.kt 了。这 4500 行代码,将会进一步揭穿 Composer 的神秘面纱

作为 Compose 探究的第一篇文章,就写到这儿吧,下一站,Composer,动身。