Jetpack Compose 深入探索系列三:Compose runtime

到目前为止,我们将 Compose runtime 在内存中维护的状态称为“Composition”,这是一个较为肤浅的概念。让我们从了解用于存储和更新Composition状态的数据结构开始探索。

slot table 和 change list

我发现这两种数据结构之间存在一些混淆,可能是由于目前缺乏关于Compose内部结构的文档。现在,我认为有必要首先澄清这一点。

slot table(插槽表)是一个优化的内存结构,runtime 使用它来存储组合的当前状态。它在初始组合时填充数据,并在每次重新组合时更新。我们可以把它看作是所有Composable函数调用的跟踪,包括它们在源码中的位置、参数、记住的值、CompositionLocals等等。它包含了在合成过程中发生的一切内容。所有这些信息将被 Composer 稍后用于生成下一个更改列表,因为对树的任何更改总是依赖于当前状态。

slot table 记录Composable的状态,而 change list 则是对节点树进行实际更改的内容。每次 Composable 函数的状态发生更改时,对应的更改操作会被添加到 change list 中。它可以理解为一个补丁文件,一旦应用,它就会更新树。所有要执行的更改都需要先被记录,然后再被应用。

最后,Recomposer协调所有这些,决定在什么时候和在什么线程上进行重组,以及在什么时候和在什么线程上应用更改。稍后会详细介绍。

slot table 深入了解

slot table 是一种为快速线性访问而优化的数据结构。它用于存储 Composition 的状态,它基于 “gap buffer”(间隙缓冲区)的思想,在文本编辑器中非常常见。它将数据存储在两个线性数组中。其中一个数组保存关于Composition中可用的 group 的信息,另一个数组存储属于每个 group插槽slot)。

在这里插入图片描述

在上一篇中,我们学习了编译器如何包装可组合函数体以使它们发出 group 。一旦 Composable 存储在内存中,这些组将为它提供唯一key,以便以后可以标识它。 group包含了 Composable 调用及其子调用的所有相关信息,并提供了关于如何处理Composable(作为一个组)的信息。根据可组合主体内部的控制流模式,group可以有不同的类型:可重启组、可移动组、可替换组、可重用组……

存储 groups 的数组使用 Int 类型,因为它只存储 group fields(表示组的元数据)。父组和子组以 group fields的形式存储。鉴于它是一个线性数据结构,父组的 group fields总是在前面,所有子组的 group fields将紧随其后。这是一种建模group树的线性方法,它有利于对子节点进行线性扫描。除非通过group锚点访问,否则对其进行随机访问是非常昂贵的。(锚点就像指针一样的存在)

另一方面,slots 数组存储这些组中的每一个group的相关数据。它存储任何类型的值(Any?),因为它意味着存储任何类型的信息。这是实际 Composition 组合数据存储的地方。存储在 groups 数组中的每个group都描述了如何在 slots 数组中查找和解释它的 slot,因为一个group总是链接到一系列的slots

slot table 依赖于一个 gap (间隙) 来读写。可以把它看成是表中的一系列位置。这个间隙会四处移动,并决定何时从哪里读取数据,何时将数据写入数组。gap 有一个指针来指示从哪里开始写入,并且可以移动它的开始和结束位置,因此表中的数据也可以被覆盖。

在这里插入图片描述

一个gap buffer是一种表示带有当前索引或光标的集合的数据结构。它在内存中用一个扁平数组实现。该扁平数组的大小大于它所表示的数据集合,未使用的空间称为gap。当需要插入新数据时,gap会移动到对应的位置,以便插入新的元素。这个过程会使得数据移动,但是由于gap的存在,移动的数据量会被最小化。这个结构的好处是,随机访问非常快速,而且插入和删除操作的时间复杂度是O(n)。

考虑以下条件逻辑:
在这里插入图片描述

由于此 Composable 被标记为 non-restartable,因此将插入一个可替换的 group(而不是一个可重启的 group)。该 group 将在 slot 表中为当前处于 “active” 状态的子元素的存储数据。如果 atrue,那么处于 “active” 状态的子元素将是 Text(a)。当条件切换时,gap 将移回 group 的起始位置,并从那里开始写入,用 Text(b) 的数据覆盖所有这些位置。

为了读写表,我们有SlotReaderSlotWriter。表可以有多个activeReader,但 只能有一个activeWriter。在每次读或写操作后,相关的 ReaderWriter都会被关闭。可以启用任意数量的 Reader 来读取数据,但为了安全起见,只有在表未被写入时才能够从中读取SlotTable会保持无效状态直到activeWriter被关闭,因为它将直接修改 groupsslots,如果我们在写的时候同时尝试从中读取,可能会导致竞争。

一个 reader 的作用类似于访问者。它跟踪正在从 groups 数组中读取的当前组、它的开始和结束位置、父组(紧挨着存储的)、当前正在读取的组中的当前slot、组的数量等等。Reader可以重新定位、跳过组、从当前 slot 中读取值、从特定索引中读取值等等。换句话说,它被用来从数组中读取有关group和它们的slot的信息。

相对的,Writer 用于向数组中写入 groups 和 slots。正如上面所述,它可以将任何类型(Any?)的数据写入表中。SlotWriter依赖于上面提到的 gap 来操作 groups 和 slots,因此它使用它们来确定在数组中进行写入的位置。

gap想象成一个可滑动可调整大小的线性数组区间Writer 维护着每个gap的起始位置、结束位置和长度,它可以通过更新起始位置和结束位置来移动gap的位置。

Writer 能够添加、替换、移动和删除 groups 和 slots。比如,想象一下向树中添加一个新的Composable 节点,或者在条件逻辑下需要替换的 Composable 节点。

Writer 可以跳过 groups 和 slots,按给定数量的位置前进,seek 到由 Anchor 确定的位置,以及许多类似的操作。

Anchor 是一个指向表中特定索引的位置的引用,SlotTablewriter 维护着指向特定位置的 Anchor 列表。同时,Anchor追踪每个group在表中的位置,也称为group索引。如果在Anchor指向的位置之前插入,移动,替换或删除groupAnchor会相应地进行更新。这个机制可以帮助writer快速访问表中的指定位置。

slot table还可以作为 composition groups的迭代器,因此它可以向工具提供关于它们的信息,以便这些工具能够检查和呈现组合的详细信息。现在是时候了解更改列表。

如果需要获取有关slot table的更多细节,建议阅读Jetpack Compose团队的Leland Richardson的这篇文章

change list

我们已经学习了关于 slot table 知识,了解了它是如何允许运行时跟踪合成的当前状态。但是,change list 的确切作用是什么呢?它是什么时候产生的?它模拟了什么?这些更改是在何时应用的,出于什么原因?我们仍然有许多事情需要搞清楚。

每当合成(或重新合成)发生时,我们的源Composable函数将被执行 和 发射(Emit)Emit 这个词意味着创建延迟的更改以更新 slot table ,最终也会更新实际生成的树。这些更改存储为一个列表。生成这个新的更改列表是基于已经存储在 slot table 中的内容。记住:对树的任何更改都必须依赖于 Composition 的当前状态。

这可以举一个移动节点的例子。假设我们要重新排列列表的Composable函数。我们需要检查该节点在表中之前的位置,删除所有这些槽,并从新位置开始再次写入它们。

换句话说,每当一个 Composable 函数发射时,它都会查看slot table ,根据当前可用的需求和信息创建延迟更改,并将其添加到包含所有更改的列表中。稍后,在 Composition 完成后,就是实例化的时候了,那些记录的更改将会被有效地执行。这时,它们会使用 Composition 的最新可用信息有效地更新slot table 。这个过程使得发射过程非常快速:它只是创建一个等待被运行的延迟操作。

由此,我们可以看出change list 是最终对slot table 进行更改的。在这之后,它将通知 Applier 更新实例化的节点树。

正如我们上面所说的,Recomposer协调这个过程,并决定在哪个线程上进行合成或重新合成,以及使用哪个线程来应用change list 中的更改。后者也是LaunchedEffect用于执行副作用的默认上下文。

投喂 Composer

注入的$composer将我们编写的 Composable 函数连接到 Compose runtime。

让我们探讨如何将节点添加到内存表示的树中。我们可以使用 Layout Composable来讨论。Layout 是 Compose UI 提供的所有UI组件的管道。下面是其代码实现:

在这里插入图片描述

Layout 使用 ReusableComposeNode 来将 LayoutNode 发射到组合中。但即使听起来像是立即创建并添加节点,它实际上是在告知 runtime 如何在组合的当前位置创建、初始化和插入节点。以下是其实现代码:

在这里插入图片描述
这里省略了一些不相关的部分,但请注意它将一切委托给currentComposer实例。我们还可以看到它是会启动一个可替换的group,以便在存储时来包装Composable的内容。在 content lambda 内发射的任何子元素都将有效地存储为 Composition 中该 group 的子元素。

其他Composable函数也是同样的发射操作。例如,remember

在这里插入图片描述

remember Composable 函数使用 currentComposer 来将用户提供的lambda返回值 缓存(记住)到 Composition 中。invalid参数强制更新值,无论它之前是否已被存储。缓存的代码实现如下:

在这里插入图片描述
它首先会在 Composition(slot table)中查找这个值。如果未找到,它就会计划安排一次对该值更新操作(换句话说,是记录它)。否则如果找到的话,就原样返回该值。

对更改进行建模

正如前面所解释的,所有委托给 currentComposer 的发射操作在内部都被建模为 Changes 并添加到列表中。Change 是一个延迟执行的函数,它可以访问当前的 ApplierSlotWriter(记住一次只有一个活动的 writter)。让我们看一下它的代码:

在这里插入图片描述

这些更改被添加到列表中(记录)。“发射”操作本质上意味着创建这些Changes,它们是延迟执行的lambdas,以潜在地从 slot table 中添加、删除、替换或移动节点,并进而通知 Applier (因此这些更改就被实现)。

出于这个原因,每当我们讨论“发射更改”时,我们可能还会使用“记录更改”或“调度更改”这样的词。它们都指的是同一件事。

在组合完成之后,一旦所有Composable函数调用完毕,所有更改都会被记录下来,此时 Applier 就会批量的应用它们

优化写入时间

如上所述,插入新节点被委托给 Composer。这意味着它总是知道什么时候已经进入了将新节点插入组合的过程。在这种情况下,Composer 可以缩短流程,并在发射更改时立即开始向slot table写入,而不是记录它们(即将它们添加到列表中稍后才进行解释)。在另一种情况下,这些更改被记录并延迟

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

川峰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值