前言
什么是 Modifier ?我相信大家已经不陌生了。
Modifier 是修饰符的意思,它可以修改 Composable 函数的行为、外观或布局,并且不改变它们的内部状态。而且 Modifier 的调用顺序还会影响到 UI 最终的显示效果,那它究竟是怎么影响的?
我们先来看看 Modifier 链的构建。
Modifier 链的构建过程
我们经常通过链式调用来创建 Modifier,比如:
Box(
Modifier
.size(100.dp)
.background(Color.Green)
.padding(6.dp)
)
它的背后会生成一条 Modifier 链,如图所示:
那它是怎么生成的呢?
靠的是 then() 方法和 CombinedModifier 类。
CombinedModifier
我们先来看看 CombinedModifier 的源码:
// Modifier.kt
class CombinedModifier(
internal val outer: Modifier,
internal val inner: Modifier
) : Modifier {
...
}
关键在于它的 outer、inner 属性,属性类型都是 Modifier,说明每个 CombinedModifier 对象可以同时指向两个 Modifier 对象,并且 CombinedModifier 自身也实现了 Modifier 接口,说明它自己能作为容器,也能被连接,从图中也能看出。
这样多次嵌套,就能够完成上图中的 Modifier 结构了。
then() 函数
有了这种结构还不够,还需要有人负责连接才行,而 then() 函数就是这个连接器。
每当调用一个修饰符函数,函数内部都会创建一个 Modifier 对象,然后调用 then() 方法:
// Modifier.kt
infix fun then(other: Modifier): Modifier =
if (other === Modifier) this else CombinedModifier(this, other)
如果传入的是空 Modifier,就直接返回当前的 Modifier,否则就会创建一个 CombinedModifier 实例,将当前 Modifier 作为 outer 参数,新的 Modifier 作为 inner 参数。
这样,通过嵌套的 CombinedModifier 对象,我们就形成了一个完整的 Modifier 链,如图所示。每个 CombinedModifier 都包含了之前所有修饰符的信息,同时保持了它们的应用顺序。
所以通过这种嵌套的 CombinedModifier 对象,就能够形成一个 Modifier 链。这个 Modifier 链包含了所有修饰符的信息,同时也保持了顺序。
LayoutNode 与 Modifier 的关系
Composable 函数在实际运行时,会生成 LayoutNode 对象,我们给 Composable 函数填写的 modifier 参数生成的 Modifier 链,最终会传递给 LayoutNode 的 modifier 属性。
来到源码看看:
// LayoutNode.kt
override var modifier: Modifier = Modifier
set(value) {
require(!isVirtual || modifier === Modifier) {
"Modifiers are not supported on virtual LayoutNodes"
}
require(!isDeactivated) {
"modifier is updated when deactivated"
}
field = value // 将传入的新 Modifier 赋值给实际存储的字段
nodes.updateFrom(value) // ⭐ 重要
layoutDelegate.updateParentData() // 更新父级数据
if (nodes.has(Nodes.IntermediateMeasure)) {
if (lookaheadRoot == null) {
lookaheadRoot = this
}
}
}
方法中核心的代码就是这一行:nodes.updateFrom(value),这行代码会处理通过 value 参数传递过来的 Modifier 链。
在点进去之前,先说一下 nodes 是什么,它是一个 NodeChain 类型的对象,它装着所有的 Modifier,并且这些 Modifier 都被 Modifier.Node 包装过。
其中尾节点一个特殊的 ContentNode,代表 Box 的内容区域。
NodeChain 类的源码:
// NodeCoordinator.kt
internal class NodeChain(val layoutNode: LayoutNode) {
internal val innerCoordinator = InnerNodeCoordinator(layoutNode)
internal var outerCoordinator: NodeCoordinator = innerCoordinator
// 维护 Modifier.Node 双向链表
internal val tail: Modifier.Node = innerCoordinator.tail
internal var head: Modifier.Node = tail
}
从源码中可以看到有 innerCoordinator、outerCoordinator 属性,它们都是 NodeCoordinator 类型的,是协调器的意思,用来协调多个 Modifier.Node 的,每一个 Modifier.Node 对象都会指向一个协调器。
那协调器是干嘛的?
是用于分层的测量与布局的工具。
innerCoordinator 负责测量 Composable 函数本身最内层的 NodeCoordinator,outerCoordinator 是最外层的 NodeCoordinator,每一个 LayoutModifierNode 都会生成一个 LayoutModifierNodeCoordinator。
了解完这些,就告一段落,继续去看 updateFrom() 方法:
/**
* 更新当前修饰符链以匹配提供的新修饰符
* 该函数通过差异比较算法高效地更新节点树,避免不必要的重建
* @param m 新的修饰符,用于更新当前修饰符链
*/
internal fun updateFrom(m: Modifier) {
// 标记是否需要同步协调器链
// 当修饰符链的结构发生变化时(添加/删除修饰符)需要重新同步协调器
var coordinatorSyncNeeded = false
// 填充链表,添加头尾节点以防止在操作过程中丢失引用
// 这是一个安全措施,确保我们总能访问链表的起点
val paddedHead = padChain()
// 获取当前的修饰符列表(旧列表)
var before = current
// 获取旧列表的大小,如果为null则为0
val beforeSize = before?.size ?: 0
// 将新的修饰符转换为向量形式
// 如果buffer为null,则创建新的可变向量
// 这种重用向量的方式可以减少内存分配
val after = m.fillVector(buffer ?: mutableVectorOf()) // ⭐ 重要
// 用于跟踪当前处理的修饰符索引
var i = 0
// 情况1: 新旧修饰符列表长度相同
// 这是最常见的情况,我们可以尝试线性差异比较(O(n)复杂度)
if (after.size == beforeSize) {
// 从链表的第一个子节点开始遍历
var node: Modifier.Node? = paddedHead.child
// 同时遍历节点链和修饰符列表
while (node != null && i < beforeSize) {
// 确保before列表不为空,因为我们已知beforeSize > 0
checkNotNull(before) { "expected prior modifier list to be non-empty" }
// 获取当前位置的旧修饰符和新修饰符
val prev = before[i]
val next = after[i]
// 比较两个修饰符,决定要采取的操作
when (actionForModifiers(prev, next)) {
// 情况1.1: 需要替换节点 - 结构性变化
ActionReplace -> {
// 回退到父节点,准备进行结构性更新
// 这意味着我们不能继续线性比较,需要切换到更复杂的差异算法
node = node.parent
break
}
// 情况1.2: 需要更新节点 - 同类型修饰符但属性变化
ActionUpdate -> {
// 更新节点以反映新修饰符的属性
// 这保留了节点实例但更新了其内部状态
updateNode(prev, next, node)
// 记录日志(如果启用)
logger?.nodeUpdated(i, i, prev, next, node)
}
// 情况1.3: 可以重用节点 - 修饰符完全相同
ActionReuse -> {
// 无需任何操作,记录日志
logger?.nodeReused(i, i, prev, next, node)
}
}
// 移动到下一个节点
node = node.child
// 移动到下一个修饰符
i++
}
// 如果线性比较中断(i < beforeSize),说明发现了结构性变化
if (i < beforeSize) {
// 标记需要同步协调器
coordinatorSyncNeeded = true
// 再次确保before列表和node不为空
checkNotNull(before) { "expected prior modifier list to be non-empty" }
checkNotNull(node) { "structuralUpdate requires a non-null tail" }
// 执行结构性更新,从中断点i开始
// 这是一个更复杂的差异算法,处理修饰符的添加、删除和移动
structuralUpdate(
i, // 从哪个索引开始更新
before, // 旧修饰符列表
after, // 新修饰符列表
node, // 从哪个节点开始更新
layoutNode.isAttached, // 布局节点是否已附加到树中
)
}
}
// 情况2: 初始化场景 - 之前没有修饰符且布局节点未附加
// 这是组件首次渲染的常见情况
else if (!layoutNode.isAttached && beforeSize == 0) {
// 需要同步协调器
coordinatorSyncNeeded = true
// 从头节点开始
var node = paddedHead
// 为每个新修饰符创建并插入节点
while (i < after.size) {
val next = after[i]
val parent = node
// 创建新节点并将其作为当前节点的子节点插入
node = createAndInsertNodeAsChild(next, parent)
// 记录插入操作
logger?.nodeInserted(0, i, next, parent, node)
i++
}
// 同步子节点类型集合
// 这有助于优化后续的布局和绘制操作
syncAggregateChildKindSet()
}
// 情况3: 清除所有修饰符
// 当新修饰符列表为空时
else if (after.size == 0) {
// 确保before列表不为空
checkNotNull(before) { "expected prior modifier list to be non-empty" }
// 从第一个子节点开始
var node = paddedHead.child
// 移除所有节点
while (node != null && i < before.size) {
// 记录移除操作
logger?.nodeRemoved(i, before[i], node)
// 分离并移除节点,然后移动到下一个节点
node = detachAndRemoveNode(node).child
i++
}
// 更新协调器的包装关系
// 由于所有修饰符都被移除,内部协调器直接与父节点的内部协调器相连
innerCoordinator.wrappedBy = layoutNode.parent?.innerCoordinator
outerCoordinator = innerCoordinator
}
// 情况4: 其他所有情况 - 需要完整的结构性更新
// 当新旧列表长度不同,且不属于上述特殊情况时
else {
// 需要同步协调器
coordinatorSyncNeeded = true
// 确保before列表不为空,如果为空则创建新的
before = before ?: MutableVector()
// 从头开始执行完整的结构性更新
structuralUpdate(
0, // 从索引0开始
before, // 旧修饰符列表
after, // 新修饰符列表
paddedHead, // 从头节点开始
layoutNode.isAttached, // 布局节点是否已附加
)
}
// 更新当前修饰符列表为新列表
current = after
// 清空旧列表并保存为缓冲区,以便下次重用
// 这减少了内存分配
buffer = before?.also { it.clear() }
// 修剪链表,移除填充的头尾节点
head = trimChain(paddedHead)
// 如果需要,同步协调器链
if (coordinatorSyncNeeded) {
syncCoordinators()
}
}
这个函数的主要工作是更新 Modifier.Node 双向链表,代码很长,但是重要的就三点。
第一点是 val after = m.fillVector(buffer ?: mutableVectorOf()),fillVector() 函数会将 Modifier 链给展开、铺平,把每一个 Modifier 装到一个“数组”中。
/**
* 将 Modifier 转换为线性的 MutableVector<Modifier.Element> 结构
* 这个函数会展平嵌套的修饰符结构,使其易于遍历和比较
*
* @param result 用于存储结果的可变向量,函数会在此向量上添加元素
* @return 填充了修饰符元素的可变向量
*/
private fun Modifier.fillVector(
result: MutableVector<Modifier.Element>
): MutableVector<Modifier.Element> {
// 确保向量容量至少为16,这有助于减少向量扩容的次数
// 如果传入的向量已经有足够容量,则保持其容量不变
val capacity = result.size.coerceAtLeast(16)
// 创建一个栈用于深度优先遍历修饰符树
// 初始容量与结果向量相同,并将当前修饰符作为起点添加到栈中
val stack = MutableVector<Modifier>(capacity).also { it.add(this) }
// 用于处理自定义修饰符的谓词函数,延迟初始化以避免不必要的分配
// 只有在遇到非标准修饰符实现时才会创建
var predicate: ((Modifier.Element) -> Boolean)? = null
// 使用深度优先遍历算法展平修饰符树
while (stack.isNotEmpty()) {
// 从栈顶取出一个修饰符
when (val next = stack.removeAt(stack.size - 1)) {
// 情况1: 组合修饰符 - 由两个修饰符组合而成
is CombinedModifier -> {
// 将内部修饰符和外部修饰符分别压入栈中
// 注意顺序:先压入inner,再压入outer
// 这确保了在展平后,修饰符的应用顺序与原始链中相同
stack.add(next.inner)
stack.add(next.outer)
}
// 情况2: 基本修饰符元素 - 直接添加到结果向量
is Modifier.Element -> result.add(next)
// 情况3: 其他类型的修饰符实现
// 这些可能是自定义的修饰符实现,不是标准的CombinedModifier或Element
else -> {
// 使用all方法遍历所有修饰符元素
// 如果predicate尚未初始化,则创建一个新的谓词函数
next.all(predicate ?: { element: Modifier.Element ->
// 将每个元素添加到结果向量
result.add(element)
// 返回true表示继续遍历
true
}.also {
// 保存谓词函数以便重用,避免重复创建
predicate = it
})
}
}
}
// 返回填充好的结果向量
return result
}
具体细节可以不用看,这里我们看一下结果就行了。
然后是第二点,beforeSize == 0 这条判断分支,这是第一次设置 Modifier 的情况。它会去遍历刚刚被铺平的 Modifier 数组,然后把每一项都装进一个 Modifier.Node 实例的里面,然后串成一个双链表。
具体的工作是在 createAndInsertNodeAsChild() 函数中完成的,我就不去看了,这里看看结果。
第三点是 syncCoordinators(),它是用于同步协调器链的,把各个 Modifier.Node 对象去跟它所属的 NodeCoordiantor 协调器做挂接的。
点进去看看源码:
/**
* 同步节点链表与协调器链的关系
* 这个函数确保每个修饰符节点都有正确的协调器,并且协调器链正确嵌套
* 从内到外遍历节点链表,为每个节点分配或更新协调器
*/
fun syncCoordinators() {
// 从最内层协调器开始,这通常是内容节点的协调器
var coordinator: NodeCoordinator = innerCoordinator
// 从尾节点的父节点开始,即最内层的修饰符节点
// tail通常是ContentNode,其parent是最内层的修饰符节点
var node: Modifier.Node? = tail.parent
// 从内到外遍历所有修饰符节点
while (node != null) {
// 尝试将当前节点转换为LayoutModifierNode
// 不是所有修饰符节点都是布局修饰符,有些可能是绘制修饰符或其他类型
val layoutmod = node.asLayoutModifierNode()
if (layoutmod != null) {
// 如果是布局修饰符节点,需要特殊处理
// 布局修饰符需要自己的协调器来处理布局相关的逻辑
// 确定下一个协调器
val next = if (node.coordinator != null) {
// 如果节点已经有协调器,重用它并更新其引用的布局修饰符节点
val c = node.coordinator as LayoutModifierNodeCoordinator
val prevNode = c.layoutModifierNode
c.layoutModifierNode = layoutmod
// 如果协调器引用的节点发生了变化,通知协调器
if (prevNode !== node) c.onLayoutModifierNodeChanged()
c
} else {
// 如果节点没有协调器,创建一个新的布局修饰符节点协调器
val c = LayoutModifierNodeCoordinator(layoutNode, layoutmod)
// 将新创建的协调器与节点关联
node.updateCoordinator(c)
c
}
// 更新协调器嵌套关系
// 当前协调器被新协调器包装
coordinator.wrappedBy = next
// 新协调器包装当前协调器
next.wrapped = coordinator
// 更新当前协调器为新协调器,继续向外层处理
coordinator = next
} else {
// 如果不是布局修饰符节点,只需要更新节点的协调器引用
// 非布局修饰符(如绘制修饰符)不需要自己的协调器,可以共享已有协调器
node.updateCoordinator(coordinator)
}
// 移动到外层节点
node = node.parent
}
// 完成所有节点的处理后,将最外层协调器与父布局节点的内层协调器连接
// 这确保了整个视图树中的协调器链是连续的
coordinator.wrappedBy = layoutNode.parent?.innerCoordinator
// 更新outerCoordinator引用,指向最外层协调器
outerCoordinator = coordinator
}
这样会保证每一个 Modifier 会受到右边离它最近的 LayoutModifierNode 布局修饰符节点的限制。因为是从内向外遍历所有修饰符节点的,先遍历到 LayoutModifierNode,会创建一个新的 NodeCoordiantor,准确来说是 LayoutModifierNodeCoordiantor,然后遍历到非布局修饰符节点时,比如 DrawModifierNode、PointerInputModifierNode,就会使用上一个布局修饰符节点所创建的 NodeCoordiantor。
调用顺序的影响
了解完这些,我们来看看调用顺序的影响。
比如我这样写:
Box(Modifier.border(1.dp, Color.Gray).padding(6.dp).size(40.dp).background(Color.Green))
以上代码的效果是:
它为什么是这样的效果?
你可能会觉得,这不是一目了然吗?先是调用 border() ,设置了灰色边框,再调用了 padding(),添加了6dp的内边距,然后调用 size() ,设置了大小为40dp,最后调用 background() ,给这块区域涂上了绿色。
那我把代码改改:
Box(
Modifier
.size(200.dp)
.background(Color.Green)
.size(80.dp)
.padding(12.dp)
.background(Color.Yellow)
.size(120.dp)
)
这样,你还能说出来吗?
在我们知道了 Modifier 的工作流程和测量和布局的过程(从外到内传递约束,从内到外确定位置),这些都不是问题。
我们先来看看上面这段代码在 LayoutNode 中的结构:
测量阶段(从外到内)
测量阶段从外到内,传递约束,size() 修饰符在有确定约束的情况下,并不会修改约束,而 padding() 修饰符会修改约束。
布局阶段(从内到外)
在布局阶段,确定各层 NodeCoordiantor 的大小时,是从内到外的,
绘制阶段(从内到外)
绘制的区域是它所属的 NodeCoordinator,所以 background(Color.Yellow)绘制到了 “橘色” 的 LayoutModifierNodeCoordiantor 上面,大小是 176x176 dp,background(Color.Green) 绘制到了 “淡绿” 的 LayoutModifierNodeCoordiantor 上面,大小是 200x200 dp。
所以它的最终结果是一个 176dp 的黄色方块盖着一个 200dp 的绿色方块:
总结:处理输入事件的PointerInputModifierNode、影响组件的绘制的DrawModifierNode,都是受到了其右侧最近的LayoutModifierNode(影响组件的测量和布局)的影响。
所以调用顺序会影响最终的UI效果。