Applier
Applier 是一个树的构建器的接口定义。
指针
其内部属性 current,作为当前操作的 node,在调用 up 和 down 操作时,该属性的值会发生变化。
interface Applier<N> {
// 当前操作应用的节点。在调用 [down] 和 [up] 时,该属性的值预期会发生变化。
val current: N
// ...
}
生命周期
为了方便监控 Applier 的更改时机,定义了更改开始和结束的生命周期方法:
// 当 [Composer] 即将开始使用这个 applier 应用更改时调用。
// 当更改完成时,将调用 [onEndChanges]。
fun onBeginChanges() {}
// 当 [Composer] 完成使用这个 applier 应用更改时调用。
// 每次调用 [onEndChanges] 之前,都会先调用 [onBeginChanges]。
fun onEndChanges() {}
遍历
包括两个操作,自上而下遍历树的 down 操作和自下而上遍历树的 up 操作
// 表示 applier 正在向下遍历树。当调用此方法时,
// [node] 预期是 [current] 的子节点,并且在此操作后,
// [node] 预期将成为新的 [current]。
fun down(node: N)
// 表示 applier 正在向上遍历树。在此操作完成后,
// [current] 应该返回操作开始时 [current] 节点的 "父节点"。
fun up()
插入
通常情况下,往一个树上插入新的节点,有两种查找插入位置的方式,即自上而下(Top-Down)和自下而上(Bottom-Up)。假设插入一个新的节点需要通知到和新节点相关的节点,那么就会产生不同的性能上的差异。
自上而下(Top-Down)插入
过程:从根节点开始,逐层向下插入节点。
步骤:
- 首先把新节点插入到父节点中。
- 然后再依次插入新节点的子节点。
举例:
假设你有一个树结构 R -> B -> (A, C):
-
第一步,把 B 插入到 R。
-
第二步,把 A 插入到 B。
-
第三步,把 C 插入到 B。
-
最终结构:
R | B / \ A C
自下而上(Bottom-Up)插入
过程:先插入子节点,然后再插入父节点。
步骤:
- 先把子节点插入到它的父节点中。
- 然后把它的父节点插入到祖父节点中。
举例:
同样的树结构 R -> B -> (A, C):
-
第一步,把 A 和 C 插入到 B。
-
第二步,把 B(已经有了 A 和 C)插入到 R。
-
最终结构:
R | B / \ A C
性能和通知的差异
当你向树中插入节点时,可能会有通知机制,这些通知会告诉父节点和子节点有新节点加入。不同插入方式对通知数量的影响如下:
自上而下的通知(Top-Down)
- 每一步都会触发通知:
- 插入 B 到 R:R 被通知。
- 插入 A 到 B:B 和 R 都被通知。
- 插入 C 到 B:B 和 R 都被通知。
- 总通知次数:随着节点数量增加,通知次数会迅速增长。例如,插入 3 个节点总共会通知 5 次。
自下而上的通知(Bottom-Up)
- 一次插入只触发一次通知:
- 插入 A 到 B:B 被通知。
- 插入 C 到 B:B 被通知。
- 插入 B(包含 A 和 C)到 R:R 被通知。
- 总通知次数:通知次数与插入的节点数量成线性关系。例如,插入 3 个节点总共会通知 3 次。
选择哪种方式
- 性能考虑:
- 自上而下(Top-Down):在每次插入时,从根节点到当前节点的所有父节点都会收到通知。如果你的树结构需要频繁地通知父节点,这种方式的通知次数会随着树的深度和节点数量迅速增加,可能导致性能问题。
- 自下而上(Bottom-Up):在插入时,先构建完整的子树,然后插入到父节点。这种方式下,每个节点的插入只触发一次通知,通知数量相对较少,因此在需要大量通知的情况下性能更高效。
- 实现的复杂性:
- 自上而下(Top-Down):的插入实现起来可能更直观,因为你按照从上到下的顺序构建树,非常符合人们对树结构的直观理解。适合树的层次结构较浅、通知需求简单的场景。
- 自下而上(Bottom-Up):需要先构建子节点并将它们插入到父节点,然后再将整个子树插入到更高层次的父节点中。这种方法可能需要额外的步骤来管理子节点的构建和插入。适合树的层次结构较深、通知需求复杂的场景,因为这种方法能减少插入过程中触发的通知次数,从而提高性能。
fun insertTopDown(index: Int, instance: N)
fun insertBottomUp(index: Int, instance: N)
移除
fun remove(index: Int, count: Int)
表示应该删除 [current] 从 [index] 到 [index] + [count] 的子节点。
移动
fun move(from: Int, to: Int, count: Int)
表示应该将 [current] 的 [count] 个子节点从索引 [from] 移动到索引 [to]。
索引 [to] 是相对于变更前的位置的。例如,要将位置 1 的元素移动到位置 2 之后,[from] 应该是 1,而 [to] 应该是 3。
如果元素是 A B C D E,调用 move(1, 3, 1),将导致元素重新排序为 A C B D E。
这里有点难以理解,假设以数组的思路去思考移动过程:
// 1. 先把 B 置空
0 1 2 3 4
A _ C D E
// 2. 把 B 添加 index = 3 的位置
0 1 2 3 4 5
A _ C B D E
// 3. 移除空槽整体向前移动
0 1 2 3 4
A C B D E
[to] 这里是移动后的索引位置,原索引位置的元素的索引会变为 [to] + 1,[from] 原来的槽会后续删除,从而导致移动索引 3,但实际上从结果上看索引是 2 的效果。
但实际的实现并不是这样的,下面的 AbstractApplier 中会解释该问题。
清理数据
fun clear()
移动到根节点并移除根节点下的所有节点,准备好当前的 [Applier] 和其根节点以便将来作为新组合的目标使用。
完整代码
@JvmDefaultWithCompatibility
interface Applier<N> {
val current: N
fun onBeginChanges() {}
fun onEndChanges() {}
fun down(node: N)
fun up()
fun insertTopDown(index: Int, instance: N)
fun insertBottomUp(index: Int, instance: N)
fun remove(index: Int, count: Int)
fun move(from: Int, to: Int, count: Int)
fun clear()
}
AbstractApplier
AbstractApplier 是 Applier 的基础实现。
AbstractApplier 内部创建了一个 MutableList(本质是一个 ArrayList)作为栈。
参数中的 root 保存到 current,current 作为指针指向当前待处理到节点。
abstract class AbstractApplier<T>(val root: T) : Applier<T> {
private val stack = mutableListOf<T>()
override var current: T = root
protected set
// ...
}
遍历的实现
// 将 current 添加到栈中,然后 current 指向新节点
override fun down(node: T) {
stack.add(current)
current = node
}
// 确保栈内非空,移除最后一个元素(栈顶),保存为 current
override fun up() {
check(stack.isNotEmpty()) { "empty stack" }
current = stack.removeAt(stack.size - 1)
}
自上而下:将 current (也就是 root)入栈,然后 current 更新为新节点(逐渐从上向下添加元素)。
自下而上:检查栈非空,移除最后一个元素,并将其保存到 current(逐渐从下向上移除元素)。
插入的实现
AbstractApplier 没有实现默认的插入逻辑,需要它的子类去实现。
移除元素的实现
// 从栈中移除指定位置的数据,或一串数据
protected fun MutableList<T>.remove(index: Int, count: Int) {
if (count == 1) {
removeAt(index)
} else {
subList(index, index + count).clear()
}
}
根据要移除的数量分为一个和多个的情况,本质上是对 MutableList 的指定索引或子列表的移除操作。
移动的实现
// **计算目标位置**:
protected fun MutableList<T>.move(from: Int, to: Int, count: Int) {
val dest = if (from > to) to else to - count
if (count == 1) {
if (from == to + 1 || from == to - 1) {
// Adjacent elements, perform swap to avoid backing array manipulations.
val fromEl = get(from)
val toEl = set(to, fromEl)
set(from, toEl)
} else {
val fromEl = removeAt(from)
add(dest, fromEl)
}
} else {
val subView = subList(from, from + count)
val subCopy = subView.toMutableList()
subView.clear()
addAll(dest, subCopy)
}
}
首先取出 dest,移动距离,如果 from 大于 to,也就是需要向原位置之前移动取 to;否则取 to - count。
从 dest 的取值可以看出,基于原位置无论是向前还是向后移动,都按照从前向后的顺序计算距离。
然后是根据移动的数量区分单个元素的情况和多个元素的情况:
-
单个元素:
-
from 如果和 to 一前一后挨着,直接交换位置即可。
-
否则,先移除 from 位置的元素,拿前面的例子说明,以
move(1, 3, 1)为例:-
首先,可以计算出 可以计算出 dest = 2;
-
然后,移除元素:
0 1 2 3 4 A B C D E 0 1 2 3 A C D E -
然后添加元素
add(dest, fromEl),这里插入位置是 dest,dest = 2,所以移动后到位置 :0 1 2 3 4 A B C D E 0 1 2 3 4 A C B D E
为什么插入位置上 dest 而不是 to 呢?add(dest, fromEl) 使用 dest 而不是 to 是因为 dest 计算的是目标插入位置在删除元素后的正确索引位置。
- 当 from > to 时:目标位置在当前元素之前。插入位置就是 to,即 dest = to。
- 当 from <= to 时:目标位置在当前元素之后。插入位置需要减去要移动的元素数量,来调整因为元素被删除导致的索引变化,
dest = to - count。
-
-
-
多个元素:
-
多个元素取子列表;
-
然后 copy 一份转换为 MutableList;
-
从原 MutableList 中删除子列表位置的数据;
-
最后整体添加到 MutableList 到 dest 位置后面。
-
举例说明, 以
move(1, 3, 2)为例:0 1 2 3 4 A B C D E // 取子列表 0 1 B C // 从原列表中删除原子列表数据 0 1 2 A D E // 添加到新位置 dest = 2 0 1 2 3 4 A D B C E
-
清理数据的实现
// 清空栈,重置当前节点为 root,调用实现的清理逻辑
final override fun clear() {
stack.clear()
current = root
onClear()
}
protected abstract fun onClear()
完整代码
abstract class AbstractApplier<T>(val root: T) : Applier<T> {
private val stack = mutableListOf<T>()
override var current: T = root
protected set
override fun down(node: T) {
stack.add(current)
current = node
}
override fun up() {
check(stack.isNotEmpty()) { "empty stack" }
current = stack.removeAt(stack.size - 1)
}
final override fun clear() {
stack.clear()
current = root
onClear()
}
protected abstract fun onClear()
protected fun MutableList<T>.remove(index: Int, count: Int) {
if (count == 1) {
removeAt(index)
} else {
subList(index, index + count).clear()
}
}
protected fun MutableList<T>.move(from: Int, to: Int, count: Int) {
val dest = if (from > to) to else to - count
if (count == 1) {
if (from == to + 1 || from == to - 1) {
// Adjacent elements, perform swap to avoid backing array manipulations.
val fromEl = get(from)
val toEl = set(to, fromEl)
set(from, toEl)
} else {
val fromEl = removeAt(from)
add(dest, fromEl)
}
} else {
val subView = subList(from, from + count)
val subCopy = subView.toMutableList()
subView.clear()
addAll(dest, subCopy)
}
}
}
UiApplier
UiApplier 是 Jetpack Compose 中用于应用 UI 树操作的组件。它的作用是将界面上的更改应用到实际的 UI 元素树中。
internal class UiApplier(
root: LayoutNode
) : AbstractApplier<LayoutNode>(root) {
override fun insertTopDown(index: Int, instance: LayoutNode) {
// Ignored. Insert is performed in [insertBottomUp] to build the tree bottom-up to avoid
// duplicate notification when the child nodes enter the tree.
}
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()
}
override fun onEndChanges() {
super.onEndChanges()
root.owner?.onEndApplyChanges()
}
}
UiApplier 实现 AbstractApplier,通过其实现,可以看出 UiApplier 没有采用自上而下的插入方式,而是采用自下而上的方式,其主要目的是为了避免新的子节点进入树时的重复通知。
current 的类型是 LayoutNode, insertBottomUp 本质逻辑在 LayoutNode 的 insertAt 方法中,其他的包括 remove、move 等操作都是直接通过 LayoutNode 的调用来完成。
最后是在树操作完成后有一个通知,onEndApplyChanges。
总结
Applier 是对树操作的抽象定义,包括了遍历、插入、移动、删除等操作的定义,其中遍历和插入分为自上而下和自下而上两种形式。
AbstractApplier 是 Applier 基础实现,主要是提供了默认的自上而下、自下而上的遍历和 move 操作的实现。
UiApplier 则是 Compose 对 UI 树操作的实现,明确了采取自下而上的插入逻辑,并在变更完成后提供通知,核心的逻辑在 LayoutNode 中。
下一篇继续分析 LayoutNode。