一、前言
本文档将以具体的修饰符链为例,剖析其从代码编写、首次组合到重组更新的完整生命周期。我们将揭示 Compose 团队为了实现极致性能(零分配更新、高效遍历)所采用的精妙设计。
假设有以下一段简单的代码:
Box(
modifier = Modifier
.padding(10.dp) // 1. 布局修改
.background(Color.Red) // 2. 绘制修改
.clickable { } // 3. 输入事件
)
这段代码看似简单,但背后发生了一系列复杂而高效的操作。我们将分三个阶段来解析它:
- 构建阶段:代码如何变成对象链。
- 物化阶段:对象链如何变成运行时的节点树(首次组合)。
- 更新阶段:当参数变化时,如何高效更新节点树(重组)。
二、配置对象的构建 (Modifier Chain)
当我们写下 .padding(10.dp).background(Color.Red) 时,实际上是在构建一个配置对象树。
2、1 Modifier 接口与伴生对象
Modifier 既是一个接口,也是一个伴生对象(Companion Object)。
// Modifier.kt
interface Modifier {
// 中缀函数:连接两个 Modifier
infix fun then(other: Modifier): Modifier =
if (other === Modifier) this else CombinedModifier(this, other)
// 伴生对象,作为链的起点
companion object : Modifier {
// ... 实现都是空的,作为哨兵或空状态
override infix fun then(other: Modifier): Modifier = other
}
}
2、2 链式调用的本质
代码执行顺序如下:
Modifier(伴生对象)- 这是一个单例,代表空 Modifier。
.padding(10.dp)- 调用
PaddingKt.padding(Modifier, 10.dp)。 - 内部创建
PaddingElement(10.dp)。 - 执行
Modifier.then(PaddingElement)。由于接收者是Modifier伴生对象,then直接返回other,即PaddingElement。
- 调用
.background(Color.Red)- 调用
BackgroundKt.background(PaddingElement, Color.Red)。 - 内部创建
BackgroundElement(Color.Red)。 - 执行
PaddingElement.then(BackgroundElement)。 - 关键点:
then返回CombinedModifier(PaddingElement, BackgroundElement)。
- 调用
.clickable { }- 创建
ClickableElement。 - 执行
CombinedModifier.then(ClickableElement)。 - 返回
CombinedModifier(CombinedModifier(Padding, Background), Clickable)。
- 创建
2、3 最终的数据结构:左倾树
最终生成的对象结构如下:
CombinedModifier
├── outer: CombinedModifier
│ ├── outer: PaddingElement(10.dp)
│ └── inner: BackgroundElement(Red)
└── inner: ClickableElement
为什么设计成这样?
- 不可变性 (Immutability):
Element和CombinedModifier都是不可变的,线程安全,且易于比较(equals)。 - 组合优于继承:通过
CombinedModifier可以无限组合不同的功能。 - 轻量级:这些对象只是“配置单”,不包含任何状态(如绘制对象、手势检测器等),创建开销极低。
三、物化 (Materialization) - 首次组合
当 Box Composable 执行时,它会创建一个 LayoutNode(UI 树的基本节点),并将上面的 Modifier 链传递给它。
LayoutNode 不会直接使用这些 Element 对象工作,它需要将它们转换成运行时的 worker 对象,即 Modifier.Node。
3、1 核心类介绍
- LayoutNode: UI 树的节点,对应一个 Composable。
- NodeChain:
LayoutNode的成员(源码中属性名为nodes),负责管理所有的 Modifier Node。 - Modifier.Node: 实际干活的类。它是可变的,有状态的,且长生命周期。
- NodeCoordinator: 布局系统的“协调者”,负责测量(Measure)和放置(Place)。
3、2 展平 (Flattening)
NodeChain 将上面的树形结构展平成线性列表。
3、2、1 调用流程
LayoutNode.modifier = modifier(属性赋值)LayoutNode.applyModifier(modifier)(私有方法)NodeChain.updateFrom(modifier)(核心入口,源码中是nodes.updateFrom(modifier))Modifier.fillVector(...)(在updateFrom内部调用)
3、2、2 为什么要展平?树形结构 vs 线性结构
在深入源码之前,我们需要理解一个核心设计哲学:配置阶段使用树形结构,运行时阶段使用线性结构。
三个关键问题:
-
为什么要将树形结构展平?
- 差异算法需要线性结构:
NodeChain.updateFrom需要逐个比对新旧 Modifier,线性列表更适合这种 O(n) 的比对操作。 - 链表是线性的:运行时的
Modifier.Node本身就是双向链表,不是树。展平后的列表可以直接映射到链表。 - 性能优化:线性遍历比树形遍历更高效,且更容易实现索引访问。
- 差异算法需要线性结构:
-
展平后,之前的树形结构还有什么用?
- 没有用了。
CombinedModifier树形结构在fillVector执行完毕后,就不再被引用了。 - 它们会被垃圾回收(GC)。
- 这体现了 "配置与运行时分离" 的设计:配置对象(树)是临时的、廉价的;运行时对象(链表)是持久的、昂贵的。
- 没有用了。
-
为什么一开始就用树形结构(而不是直接用列表)?
在深入分析之前,我们需要先理解不可变对象的概念。
不可变对象是指一旦创建,其状态就不能被修改的对象。
Kotlin 中的不可变性:
// 可变对象 class MutablePerson { var name: String = "Alice" // var = 可变 var age: Int = 25 } val person1 = MutablePerson() person1.name = "Bob" // ✅ 可以修改 person1.age = 26 // ✅ 可以修改 // 不可变对象 class ImmutablePerson( val name: String, // val = 不可变 val age: Int // val = 不可变 ) val person2 = ImmutablePerson("Alice", 25) // person2.name = "Bob" // ❌ 编译错误!val 不能重新赋值 // person2.age = 26 // ❌ 编译错误!val 不能重新赋值关键区别:
var(variable):可变,可以重新赋值val(value):不可变,一旦赋值就不能改变
不可变集合:
// 可变列表 val mutableList = mutableListOf(1, 2, 3) mutableList.add(4) // ✅ 可以添加元素 mutableList[0] = 10 // ✅ 可以修改元素 // 不可变列表 val immutableList = listOf(1, 2, 3) // immutableList.add(4) // ❌ 编译错误!没有 add 方法 // immutableList[0] = 10 // ❌ 编译错误!不能修改元素如何"修改"不可变对象?
不可变对象不能被修改,但可以创建一个新的对象:
val list1 = listOf(1, 2, 3) // 不能直接修改,但可以创建新列表 val list2 = list1 + 4 // 创建新列表 [1, 2, 3, 4] println(list1) // [1, 2, 3] - 原列表不变 println(list2) // [1, 2, 3, 4] - 新列表不可变性的代价:
// 每次添加元素都要复制整个列表 val list1 = listOf(1) val list2 = list1 + 2 // 复制 1 个元素 val list3 = list2 + 3 // 复制 2 个元素 val list4 = list3 + 4 // 复制 3 个元素 // ... // 第 n 次:复制 n-1 个元素 // 总时间复杂度:O(n²)为什么不可变对象很重要?
-
线程安全:不可变对象可以在多个线程中安全共享,无需加锁。
-
可预测性:对象的状态不会意外改变,便于推理和调试。
-
缓存友好:不可变对象可以被安全地缓存和复用。
-
Diff 算法友好:Compose 的重组依赖
equals()比较,不可变对象保证比较结果稳定。
现在,让我们从源码中找到
Modifier不可变性的证据。源码证据 1:官方文档注释
// Modifier.kt 第 33-35 行 /** * An ordered, immutable collection of [modifier elements][Modifier.Element] that decorate or add * behavior to Compose UI elements. */ @Stable interface Modifier { ... }注释明确说明:
Modifier是一个 "immutable collection"(不可变集合)。源码证据 2:CombinedModifier 的属性是 val(不可变)
// Modifier.kt 第 317-321 行 class CombinedModifier( internal val outer: Modifier, // val = 不可变 internal val inner: Modifier // val = 不可变 ) : Modifierouter和inner都是val,一旦创建就不能修改。源码证据 3:then 方法返回新对象
// Modifier.kt 第 97-98 行 infix fun then(other: Modifier): Modifier = if (other === Modifier) this else CombinedModifier(this, other)then方法从不修改this,而是返回一个新的CombinedModifier对象。源码证据 4:@Stable 注解
@Stable // 表示这个类型是稳定的,不会变化 interface Modifier { ... }@Stable是 Compose 的注解,表示这个类型一旦创建,其内容就不会改变。
为什么不可变很重要?
-
Compose的差异算法依赖不可变性:如果修饰符可以被修改,
equals()比较就会失效,无法正确判断是否需要更新。 -
线程安全:不可变对象可以在多个线程中安全共享,无需加锁。
-
可预测性:一个修饰符对象在整个生命周期内都不会改变,便于推理和调试。
为什么不可变性导致列表方案效率低?
因为不可变意味着不能原地追加。如果用列表实现:
class ListModifier(val elements: List<Element>) : Modifier { // val = 不可变 override fun then(other: Modifier): Modifier { // 不能这样做:elements.add(other) // ❌ elements 是 val,不能修改! // 必须复制整个列表 val newList = elements.toMutableList() // 复制所有元素 newList.add(other as Element) return ListModifier(newList.toList()) // 返回新的不可变列表 } }而树形结构(
CombinedModifier)只需要创建一个新节点,持有对原有对象的引用,无需复制:class CombinedModifier(val outer: Modifier, val inner: Modifier) : Modifier { // 直接持有引用,不复制任何内容 }关键对比:
方案 每次 then的操作时间复杂度 列表(不可变) 复制整个列表 + 追加新元素 O(n) 树形结构 创建一个 CombinedModifier 对象 O(1) 总结:树形结构让每次
then调用都是 O(1),而展平只在最后进行一次(O(n))。
3、2、3 源码解析
// NodeChain.kt 第 749-777 行
private fun Modifier.fillVector(
result: MutableVector<Modifier.Element>,
stack: MutableVector<Modifier>,
): MutableVector<Modifier.Element> {
stack.add(this) // 1. 首先把整个 Modifier 链压入栈
while (stack.isNotEmpty()) {
when (val next = stack.removeAt(stack.size - 1)) { // 2. 从栈顶取出
is CombinedModifier -> {
// 3. 如果是 CombinedModifier,先压入 inner,再压入 outer
// 因为栈是(后进先出),所以 outer 会先被取出处理
stack.add(next.inner)
stack.add(next.outer)
}
is Modifier.Element -> result.add(next) // 4. 如果是叶子节点,加入结果列表
else -> next.all { element ->
result.add(element)
true
}
}
}
return result
}
3、2、4 详细执行过程图解
让我们用具体的例子来演示 fillVector 如何将左倾树展平:
输入的树形结构(第一阶段构建的结果):
CombinedModifier(C2)
├── outer: CombinedModifier(C1)
│ ├── outer: PaddingElement
│ └── inner: BackgroundElement
└── inner: ClickableElement
执行过程:
| 步骤 | 操作 | 栈状态(栈顶在右) | 结果列表 |
|---|---|---|---|
| 0 | 初始化,压入根节点 | [C2] | [] |
| 1 | 弹出 C2,是 CombinedModifier,压入 inner(Clickable) 和 outer(C1) | [Clickable, C1] | [] |
| 2 | 弹出 C1,是 CombinedModifier,压入 inner(Background) 和 outer(Padding) | [Clickable, Background, Padding] | [] |
| 3 | 弹出 Padding,是 Element,加入结果 | [Clickable, Background] | [Padding] |
| 4 | 弹出 Background,是 Element,加入结果 | [Clickable] | [Padding, Background] |
| 5 | 弹出 Clickable,是 Element,加入结果 | [] | [Padding, Background, Clickable] |
| 6 | 栈空,循环结束 | - | - |
最终结果:[PaddingElement, BackgroundElement, ClickableElement]
为什么这样设计?
-
栈 + 先压 inner 再压 outer = 深度优先的左侧优先遍历
- 因为栈是 LIFO,后压入的
outer会先被取出。 - 这确保了链式调用中靠左的 Modifier 先被处理。
- 因为栈是 LIFO,后压入的
-
保持代码顺序一致性
Modifier.padding().background().clickable()这个链式调用,用户期望 Padding 最先生效,然后是 Background,最后是 Clickable。- 展平后的列表顺序
[Padding, Background, Clickable]正好反映了这一用户预期。
-
与测量/绘制顺序的关系
- 列表中靠前的 Modifier 会在 Node 链表中更靠近 外侧(Outer)。
- 测量时,约束从外向内传递,所以 Padding 的约束修改会影响后续所有 Modifier。
- 绘制时,从内向外绘制,所以 Background 画在 Padding 内部的区域上。
3、3 创建节点
NodeChain 遍历展平后的 Element 列表,为每个 Element 创建对应的 Node,并将它们插入到双向链表中。
3、3、1 什么是哨兵节点 (Sentinel Node)?
哨兵节点(也称为哑节点或虚拟节点)是一种特殊节点,它不存储实际数据,而是作为链表操作的辅助节点。
为什么需要哨兵节点?
在双向链表操作中,如果没有哨兵节点,插入和删除操作需要特别处理头节点和尾节点的情况,这会增加代码的复杂性。
示例对比:
// 没有哨兵节点的双向链表
Head <-> Node1 <-> Node2
// 插入新节点到头部时需要特殊处理
if (head == null) {
// 链表为空
head = newNode
tail = newNode
} else {
// 链表不为空
newNode.next = head
head.prev = newNode
head = newNode
}
// 插入新节点到尾部时也需要特殊处理
if (tail == null) {
// 链表为空
head = newNode
tail = newNode
} else {
// 链表不为空
tail.next = newNode
newNode.prev = tail
tail = newNode
}
// 有哨兵节点的双向链表
SentinelHead <-> Head <-> Node1 <-> Node2 <-> Tail
// 插入新节点到头部时无需特殊处理
// (假设在 SentinelHead 和 Head 之间插入 newNode)
newNode.next = head
newNode.prev = sentinelHead
head.prev = newNode
sentinelHead.next = newNode
// 插入新节点到尾部时也无需特殊处理
// (假设在 Node2 和 Tail 之间插入 newNode)
newNode.next = tail
newNode.prev = tail.prev
tail.prev.next = newNode
tail.prev = newNode
通过哨兵节点,我们可以统一处理链表的各种操作,而不需要频繁检查头尾节点是否为空。
NodeChain 中的哨兵节点设计:
在 NodeChain 中,有两个关键的哨兵节点:
sentinelHead:链表头部的哨兵节点tail:链表尾部的哨兵节点(同时也是InnerCoordinator的tail)
// NodeChain.kt 第 33-39 行
internal class NodeChain(val layoutNode: LayoutNode) {
private val sentinelHead =
object : Modifier.Node() {
override fun toString() = "<Head>"
}
.apply { aggregateChildKindSet = 0.inv() }
// ...
internal val tail: Modifier.Node = innerCoordinator.tail
internal var head: Modifier.Node = tail
private set
}
工作流程:
padChain():在updateFrom开始时调用,将sentinelHead插入到链表头部。- 链表操作:所有的 Node 插入/删除操作都在
sentinelHead和tail之间进行。 trimChain():在updateFrom结束时调用,移除sentinelHead,并将head指向真正的第一个节点。
这种设计简化了链表操作,避免了对头尾节点的特殊处理。
调用流程
NodeChain.updateFrom(modifier)(入口)m.fillVector(buffer, stack)(展平树形结构,获取 Element 列表)- 进入初始化路径 (因为
beforeSize == 0且layoutNode.applyingModifierOnAttach == true) - 循环调用
createAndInsertNodeAsChild(element, parent) element.create()(调用 Element 接口的工厂方法)insertChild(node, parent)(插入双向链表)syncAggregateChildKindSet()(更新 KindSet)
3、3、2 源码解析
1. updateFrom 中的初始化路径 (NodeChain.kt 第 95-110 行)
// NodeChain.kt 第 95-110 行
internal fun updateFrom(m: Modifier) {
// ... 省略前面的代码
// 展平 Modifier 树形结构
val after = m.fillVector(buffer ?: mutableVectorOf(), stack)
// ... 省略中间的判断逻辑
// 初始化路径:beforeSize == 0 且 layoutNode.applyingModifierOnAttach == true
if (layoutNode.applyingModifierOnAttach && beforeSize == 0) {
// 常见情况:首次设置 Modifier 链
coordinatorSyncNeeded = true
var node = paddedHead // 从哨兵节点开始
while (i < after.size) {
val next = after[i] // 获取下一个 Element
val parent = node // 当前节点作为父节点
node = createAndInsertNodeAsChild(next, parent) // 创建并插入 Node
logger?.nodeInserted(0, i, next, parent, node)
i++
}
syncAggregateChildKindSet() // 更新 KindSet
}
// ... 省略后面的代码
}
2. createAndInsertNodeAsChild 方法 (NodeChain.kt 第 425-440 行)
// NodeChain.kt 第 425-440 行
private fun createAndInsertNodeAsChild(
element: Modifier.Element,
parent: Modifier.Node,
): Modifier.Node {
// 根据类型创建 Node
val node =
when (element) {
is ModifierNodeElement<*> ->
// 新式 Modifier:调用 create() 方法创建 Node
element.create().also {
// 计算 Node 的 KindSet(表示 Node 实现了哪些接口)
it.kindSet = calculateNodeKindSetFromIncludingDelegates(it)
}
else ->
// 旧式 Modifier:用 BackwardsCompatNode 包装
BackwardsCompatNode(element)
}
// 检查:Node 不能已经 attached
checkPrecondition(!node.isAttached) {
"A ModifierNodeElement cannot return an already attached node from create() "
}
// 标记:等待 attach 后进行无效化
node.insertedNodeAwaitingAttachForInvalidation = true
// 插入双向链表
return insertChild(node, parent)
}
3. insertChild 方法 (NodeChain.kt 第 445-458 行)
// NodeChain.kt 第 445-458 行
/**
* 将 [node] 插入为 [parent] 的子节点。
*
* 插入前:Head... -> parent -> ...Tail
* 插入后:Head... -> parent -> node -> ...Tail
*/
private fun insertChild(node: Modifier.Node, parent: Modifier.Node): Modifier.Node {
val theChild = parent.child // 获取 parent 原来的子节点
if (theChild != null) {
// 如果 parent 原来有子节点,需要调整指针
theChild.parent = node // 原子节点的父节点指向新节点
node.child = theChild // 新节点的子节点指向原子节点
}
// 设置新节点和父节点的双向链接
parent.child = node
node.parent = parent
return node
}
4. ModifierNodeElement 的设计 (以 Padding 为例)
// Padding.kt (示例)
private data class PaddingElement(val padding: Dp) : ModifierNodeElement<PaddingNode>() {
// 创建对应的 Node
override fun create(): PaddingNode = PaddingNode(padding)
// 更新 Node (重组时用到)
override fun update(node: PaddingNode) {
node.padding = padding
}
}
// PaddingNode 的定义
class PaddingNode(var padding: Dp) : Modifier.Node(), LayoutModifierNode {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
// 修改约束
val newConstraints = constraints.constrainWidth(padding.roundToPx())
val placeable = measurable.measure(newConstraints)
return layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
}
3、3、3 详细执行过程图解
让我们用具体的例子来演示 Node 的创建和插入过程:
输入的 Element 列表(展平后的结果):
[PaddingElement, BackgroundElement, ClickableElement]
执行过程:
| 步骤 | 操作 | 链表状态 (parent -> child) | 说明 |
|---|---|---|---|
| 0 | 初始化 | Sentinel -> Tail | 哨兵节点和尾节点 |
| 1 | 创建 PaddingNode,插入到 Sentinel 后 | Sentinel -> PaddingNode -> Tail | parent = Sentinel |
| 2 | 创建 BackgroundNode,插入到 PaddingNode 后 | Sentinel -> PaddingNode -> BackgroundNode -> Tail | parent = PaddingNode |
| 3 | 创建 ClickableNode,插入到 BackgroundNode 后 | Sentinel -> PaddingNode -> BackgroundNode -> ClickableNode -> Tail | parent = BackgroundNode |
最终生成的 Node 双向链表:
Sentinel <-> PaddingNode <-> BackgroundNode <-> ClickableNode <-> Tail
↑ ↑ ↑ ↑
parent parent parent parent
双向链表的指针关系:
PaddingNode:
- parent = Sentinel
- child = BackgroundNode
BackgroundNode:
- parent = PaddingNode
- child = ClickableNode
ClickableNode:
- parent = BackgroundNode
- child = Tail
3、4 协调者 (Coordinator) 的绑定
如果说 NodeChain 是逻辑上的处理链,那么 CoordinatorChain 就是物理上的布局层级。
3、4、1 通俗理解:Coordinator 是什么?
想象一个 "俄罗斯套娃" 或者 "洋葱":
- 最核心 (LayoutNode/InnerCoordinator):这是组件的本体(比如一个 Text 或 Image)。
- 外层包裹 (PaddingCoordinator):你在本体外面包了一层泡沫纸(Padding)。
- 更外层包裹 (OffsetCoordinator):你把包了泡沫纸的本体放进了一个盒子(Offset)。
每一个包裹层,就是一个 Coordinator。
- Coordinator 负责管理"位置"和"大小"。
- Modifier Node 负责定义"规则"(比如 Padding 多少,Offset 多少)。
并不是所有的 Modifier 都需要创建一个新的包裹层。只有 LayoutModifier(如 padding, offset, size)这种会改变大小或位置的 Modifier,才需要一个新的 Coordinator。
像 background、clickable 这种 Modifier,它们不改变布局,就像是给盒子贴了个标签或者涂了层颜色,不需要额外的盒子包裹,所以它们直接依附在最近的 Coordinator 上。
3、4、2 为什么需要 Coordinator?
如果没有 Coordinator,所有的事情都混在一起。比如,你点击一个按钮:
- 问题:点击事件的坐标是
(100, 100)。这个坐标是相对于谁的?是相对于整个屏幕?还是相对于添加了 Padding 之后的区域? - 解决:每个 Coordinator 都有自己的坐标系。
PaddingCoordinator知道自己相对于父容器的位置。- 它负责把父容器传来的触摸事件坐标,转换成内部内容的坐标(减去 padding)。
- 它负责把内部内容的大小,加上 padding,报告给父容器。
3、4、3 详细图解:从 Node 到 Coordinator
假设代码如下:
Box(
Modifier
.padding(10.dp) // LayoutModifier -> 产生 Coordinator
.background(Red) // DrawModifier -> 依附最近的 Coordinator
.offset(x = 5.dp) // LayoutModifier -> 产生 Coordinator
.clickable { } // PointerInput -> 依附最近的 Coordinator
)
生成的 Node 链表 (逻辑结构):
Head -> PaddingNode -> BackgroundNode -> OffsetNode -> ClickableNode -> Tail
生成的 Coordinator 链表 (物理布局结构):
LayoutNode (整体)
|
+-- PaddingCoordinator (最外层)
|
+-- OffsetCoordinator (中间层)
|
+-- InnerCoordinator (最内层,即 Box 本体)
绑定关系 (谁依附谁):
- PaddingNode 绑定的 Coordinator 是
PaddingCoordinator。 - BackgroundNode 没有自己的 Coordinator,依附于
PaddingCoordinator(因为它是画在 Padding 层里的)。 - OffsetNode 绑定的 Coordinator 是
OffsetCoordinator。 - ClickableNode 没有自己的 Coordinator,依附于
InnerCoordinator(因为它是点在 Box 本体上的)。
3、4、4 Coordinator 如何工作?(源码流程)
让我们看 NodeChain.syncCoordinators() 是如何把 Node 变成 Coordinator 链的。这个方法从内向外(从 Tail 到 Head)遍历 Node。
// NodeChain.kt (简化伪代码)
fun syncCoordinators() {
// 1. 从最里面的 InnerCoordinator 开始
var currentCoordinator = innerCoordinator
// 2. 从尾部向头部遍历所有 Node
var node = tail.parent
while (node != null) {
// 3. 判断是否是 LayoutModifier (比如 Padding, Offset)
if (node is LayoutModifierNode) {
// 是 LayoutModifier,必须穿一件"新衣服" (创建新的 Coordinator)
val newCoordinator = LayoutModifierNodeCoordinator(node)
// 链接链表:外层包内层
newCoordinator.wrapped = currentCoordinator
currentCoordinator.wrappedBy = newCoordinator
// 更新当前 Coordinator 指针
currentCoordinator = newCoordinator
} else {
// 不是 LayoutModifier (比如 Background),直接依附在当前的"衣服"上
node.updateCoordinator(currentCoordinator)
}
node = node.parent
}
// 4. 设置最外层的 Coordinator
outerCoordinator = currentCoordinator
}
四、重组与更新 (Recomposition & Update)
现在,假设我们修改了代码,背景色变成了蓝色:
.padding(10.dp)
.background(Color.Blue) // Red -> Blue
.clickable { }
重组发生时,会生成一套新的 Element 对象链。LayoutNode.setModifier 会调用 NodeChain.updateFrom(newModifier)。
这是 Compose 性能优化的核心所在:零分配更新 (Zero-allocation Update)。
4、1 差异比对 (Diffing) - Fast Path
NodeChain 会同时持有 before (旧 Element 列表) 和 after (新 Element 列表)。
调用流程
NodeChain.updateFrom(newModifier)(入口)newModifier.fillVector(buffer, stack)(展平新的 Modifier 树)- 进入更新路径 (因为
beforeSize != 0) structuralUpdate(before, after)(核心 Diff 算法)- 根据 Diff 结果执行相应的操作 (创建/更新/删除 Node)
4、1、1 源码解析
1、updateFrom 方法深度解析
updateFrom 是 NodeChain 更新的入口方法,它处理四种主要场景:
// NodeChain.kt
internal fun updateFrom(m: Modifier) {
// 1. 临时填充链表头部,方便 Diff 操作
// padChain() 会在 head 前面加一个 sentinelHead,防止链表被剪断
val paddedHead = padChain()
// 2. 展平新的 Modifier 树
// 使用 fillVector 将树形结构的 Modifier 展平成线性列表 'after'
// 'buffer' 和 'stack' 是复用的,避免频繁分配内存
var before = current
val beforeSize = before?.size ?: 0
val after = m.fillVector(buffer ?: mutableVectorOf(), stack)
var i = 0
// === 场景一:快速路径 (Fast Path) ===
if (after.size == beforeSize) {
// 假设长度相同意味着结构没变,尝试 O(n) 的线性更新
var node: Modifier.Node? = paddedHead.child
while (node != null && i < beforeSize) {
val prev = before[i]
val next = after[i]
when (actionForModifiers(prev, next)) {
ActionReplace -> {
// 遇到类型不匹配,快速路径失败!
// 记录失败位置 i,后面将从这个位置开始进行 structuralUpdate
// node 回退一步,准备进行 Diff
node = node.parent
break
}
ActionUpdate -> {
// 类型相同但内容变了,复用 Node 并更新
updateNode(prev, next, node)
}
ActionReuse -> {
// 完全相同,直接复用
}
}
node = node.child
i++
}
// 如果 i < beforeSize,说明循环被 break 了(遇到了 ActionReplace)
if (i < beforeSize) {
// 调用 structuralUpdate 处理剩余部分的 Diff
// i 是失败的索引,node 是失败前的最后一个有效节点(作为 tail)
structuralUpdate(i, before, after, node, !layoutNode.applyingModifierOnAttach)
}
} else if (layoutNode.applyingModifierOnAttach && beforeSize == 0) {
// === 场景二:初始化路径 (Init Path) ===
// 这是一个非常常见的场景:LayoutNode 刚创建,第一次设置 Modifier
// 直接循环插入所有节点,不需要 Diff
var node = paddedHead
while (i < after.size) {
val next = after[i]
val parent = node
node = createAndInsertNodeAsChild(next, parent)
i++
}
// 更新 KindSet
syncAggregateChildKindSet()
} else if (after.size == 0) {
// === 场景三:完全清理路径 (Clear Path) ===
// 新 Modifier 是空的,移除所有旧节点
var node = paddedHead.child
while (node != null && i < before.size) {
node = detachAndRemoveNode(node).child
i++
}
} else {
// === 场景四:慢速路径 (Slow Path) ===
// 长度不同,且不是初始化,直接进行完整的 structuralUpdate
before = before ?: MutableVector()
structuralUpdate(0, before, after, paddedHead, !layoutNode.applyingModifierOnAttach)
}
// 3. 收尾工作
current = after
buffer = before?.also { it.clear() } // 复用 before 作为下一次的 buffer
head = trimChain(paddedHead) // 移除 sentinelHead
// 4. 同步 Coordinator
// 如果发生了结构性变化(structuralUpdate 被调用),需要重新链接 Coordinator
if (coordinatorSyncNeeded) {
syncCoordinators()
}
}
关键点总结:
- Pad & Trim:
padChain()和trimChain()这一对操作是为了简化链表操作。在更新开始前,给链表头加一个哨兵节点 (sentinelHead),这样所有的插入/删除操作都可以统一成"在某个节点后面操作",避免了处理head为空的特殊情况。更新结束后再移除哨兵。 - Fast Path 优先: Compose 假设大部分重组不会改变 Modifier 的结构(类型和顺序),只会改变参数。因此优先尝试 O(n) 的线性比对。
- Fail-Fast: 一旦 Fast Path 遇到不匹配 (
ActionReplace),立即停止,并记录停止的位置i。后续的structuralUpdate只需要处理从i开始的剩余部分,而不是整个列表。这是一种非常高效的增量更新策略。 - Resource Recycling:
buffer和stack都是成员变量,被反复复用,确保了整个更新过程的零内存分配。
2、actionForModifiers 方法详解
这个方法决定了两个 Element 之间的关系:复用、更新还是替换。
// NodeChain.kt
internal fun actionForModifiers(prev: Modifier.Element, next: Modifier.Element): Int {
return if (prev == next) {
ActionReuse
} else if (areObjectsOfSameType(prev, next)) {
ActionUpdate
} else {
ActionReplace
}
}
判断逻辑:
prev == next: 如果两个 Element 相等(equals返回true),则直接复用 (ActionReuse)。areObjectsOfSameType(prev, next): 如果类型相同,则更新 (ActionUpdate)。- 否则: 类型不同,必须替换 (
ActionReplace)。
areObjectsOfSameType 的实现:
// Modifier.kt
internal fun areObjectsOfSameType(a: Any?, b: Any?): Boolean {
return a != null && b != null && a::class == b::class
}
3、structuralUpdate 方法详解
structuralUpdate 是 Compose Modifier 系统中实现零分配更新的核心算法。它使用 Myers Diff 算法来计算两个 Element 列表之间的最小差异操作。
// NodeChain.kt
private fun structuralUpdate(
offset: Int,
before: MutableVector<Modifier.Element>,
after: MutableVector<Modifier.Element>,
tail: Modifier.Node,
shouldAttachOnInsert: Boolean,
) {
// 获取 Differ 实例
// getDiffer 会复用 cachedDiffer 对象,避免每次都 new 一个 Differ,实现零分配
val differ = getDiffer(tail, offset, before, after, shouldAttachOnInsert)
// 2. 执行 Diff (关键步骤)
// 这里调用了 MyersDiff.kt 中的 executeDiff 方法
// 它是连接 Diff 算法和 Differ 回调的桥梁
executeDiff(before.size - offset, after.size - offset, differ)
// 3. 更新 KindSet
// Modifier 链结构变化后,LayoutNode 的聚合能力集可能发生变化,需要同步
syncAggregateChildKindSet()
}
4、Differ 内部类详解
getDiffer 会复用 cachedDiffer 对象,避免每次都创建Differ,实现零分配。Differ实现了 DiffCallback 接口,定义了如何处理差异算法计算出的差异操作。
// NodeChain.kt
private inner class Differ(
var node: Modifier.Node, // 当前遍历到的 Node
var offset: Int, // 偏移量
var before: MutableVector<Modifier.Element>, // 旧列表
var after: MutableVector<Modifier.Element>, // 新列表
var shouldAttachOnInsert: Boolean, // 插入时是否立即 attach
) : DiffCallback {
// 判断两个 Element 是否是同一个类型(不是 ActionReplace)
override fun areItemsTheSame(oldIndex: Int, newIndex: Int): Boolean {
return actionForModifiers(before[offset + oldIndex], after[offset + newIndex]) !=
ActionReplace
}
// 插入新 Node
override fun insert(newIndex: Int) {
val index = offset + newIndex
val parent = node
node = createAndInsertNodeAsChild(after[index], parent)
logger?.nodeInserted(index, index, after[index], parent, node)
if (shouldAttachOnInsert) {
// 立即 attach 并更新 Coordinator 链
val childCoordinator = node.child!!.coordinator!!
val layoutmod = node.asLayoutModifierNode()
if (layoutmod != null) {
// 如果是 LayoutModifier,需要创建新的 Coordinator
val thisCoordinator = LayoutModifierNodeCoordinator(layoutNode, layoutmod)
node.updateCoordinator(thisCoordinator)
propagateCoordinator(node, thisCoordinator)
thisCoordinator.wrappedBy = childCoordinator.wrappedBy
thisCoordinator.wrapped = childCoordinator
childCoordinator.wrappedBy = thisCoordinator
} else {
node.updateCoordinator(childCoordinator)
}
node.markAsAttached()
node.runAttachLifecycle()
autoInvalidateInsertedNode(node)
} else {
// 延迟 attach
node.insertedNodeAwaitingAttachForInvalidation = true
}
}
// 移除旧 Node
override fun remove(atIndex: Int, oldIndex: Int) {
val toRemove = node.child!!
logger?.nodeRemoved(oldIndex, before[offset + oldIndex], toRemove)
if (toRemove.isKind(Nodes.Layout)) {
// 如果是 LayoutModifier,需要更新 Coordinator 链
val removedCoordinator = toRemove.coordinator!!
val parentCoordinator = removedCoordinator.wrappedBy
val childCoordinator = removedCoordinator.wrapped!!
parentCoordinator?.wrapped = childCoordinator
childCoordinator.wrappedBy = parentCoordinator
propagateCoordinator(node, childCoordinator)
}
node = detachAndRemoveNode(toRemove)
}
// 处理相同的 Node(可能是 Update 或 Reuse)
override fun same(oldIndex: Int, newIndex: Int) {
node = node.child!!
val prev = before[offset + oldIndex]
val next = after[offset + newIndex]
if (prev != next) {
// 内容不同,需要更新
updateNode(prev, next, node)
logger?.nodeUpdated(offset + oldIndex, offset + newIndex, prev, next, node)
} else {
// 完全相同,直接复用
logger?.nodeReused(offset + oldIndex, offset + newIndex, prev, next, node)
}
}
}
Differ 的关键逻辑:
areItemsTheSame: 判断两个 Element 是否是同一个类型。如果不是ActionReplace,则认为是同一个类型。insert: 创建新 Node 并插入链表。如果是LayoutModifier,需要创建新的Coordinator并更新 Coordinator 链。remove: 移除旧 Node。如果是LayoutModifier,需要更新 Coordinator 链。same: 处理相同的 Node。如果内容不同,调用updateNode更新;如果完全相同,直接复用。
5、executeDiff
executeDiff 定义在 MyersDiff.kt 中,它是整个 Diff 过程的指挥官。
// MyersDiff.kt
internal fun executeDiff(oldSize: Int, newSize: Int, callback: DiffCallback) {
// 1. 计算差异路径 (Calculate)
// 使用 Myers 算法计算出从旧列表变换到新列表的最优路径(对角线)
// 返回一个 IntStack,里面存储了差异路径的关键点
val diagonals = calculateDiff(oldSize, newSize, callback)
// 2. 应用差异 (Apply)
// 遍历计算出的路径,回调 callback 的方法
applyDiff(diagonals, callback)
}
6、calculateDiff 算法核心解析
calculateDiff 实现了著名的 Myers Diff 算法(这也是 git diff 和 DiffUtil 背后的算法)。它的目标是找到从旧列表变到新列表的最短编辑脚本 (Shortest Edit Script)。
算法将问题抽象为一个网格图:
- X 轴代表旧列表。
- Y 轴代表新列表。
- 从 (0, 0) 走到 (oldSize, newSize)。
- 向右走一步 = 删除 (Remove)。
- 向下走一步 = 插入 (Insert)。
- 沿对角线走 = 相同 (Same),不消耗步数。
- 目标是找到步数最少的路径。
分治与双向搜索 (Divide and Conquer with Bidirectional Search)
为了降低空间复杂度(从 O(N^2) 降低到 O(N)),Compose 的实现采用了线性空间细化版 (Linear Space Refinement)。它不是直接计算整条路径,而是通过双向搜索找到路径的中点 (Middle Snake),然后将问题一分为二,递归求解。
// MyersDiff.kt 第 47-98 行
private fun calculateDiff(oldSize: Int, newSize: Int, cb: DiffCallback): IntStack {
// ... 初始化数据结构 ...
// 使用 Stack 模拟递归,避免 StackOverflow
val stack = IntStack(max * 4)
stack.pushRange(0, oldSize, 0, newSize)
val diagonals = IntStack(max * 3)
while (stack.isNotEmpty()) {
// 取出一个待处理的矩形区域
val newEnd = stack.pop()
val newStart = stack.pop()
val oldEnd = stack.pop()
val oldStart = stack.pop()
// 核心:寻找"中点蛇" (Middle Snake)
// midPoint 会尝试从左上角和右下角同时向中间搜索
// 这里的 forward 和 backward 数组用于记录搜索的前沿 (K-lines)
val found = midPoint(oldStart, oldEnd, newStart, newEnd, cb, forward, backward, snake.data)
if (found) {
// 如果找到了中点,将中点记录下来 (这就是一条对角线)
if (snake.diagonalSize > 0) {
snake.addDiagonalToStack(diagonals)
}
// 将问题分割成两部分(左上和右下),压入栈中继续处理
// Part 1: 左上角 -> Snake起点
stack.pushRange(oldStart, snake.startX, newStart, snake.startY)
// Part 2: Snake终点 -> 右下角
stack.pushRange(snake.endX, oldEnd, snake.endY, newEnd)
}
}
// 对结果排序并返回
diagonals.sortDiagonals()
return diagonals
}
辅助数据结构:
stack: 存储待处理的子区域坐标。forward/backward(CenteredArray): 这是 Myers 算法的核心优化。它只需要存储当前搜索步数 (D) 下,每条对角线 (k-line) 能到达的最远位置。因为下标可能是负数(k = x - y),所以用CenteredArray来支持负数索引。snake: 临时存储找到的 Middle Snake 信息(startX, startY, endX, endY, diagonalSize)。
midPoint 函数:
这是真正干活的地方。它交替进行正向搜索(forward)和反向搜索(backward),步步为营,直到两个搜索方向在某条对角线上相遇。相遇点就是最优路径的中点。
// MyersDiff.kt 第 132-161 行
private fun midPoint(...): Boolean {
// ... 初始化 ...
// d 是步数 (Edit Distance的一半)
for (d in 0 until max) {
// 正向搜索一步
if (forward(..., d, snake)) return true
// 反向搜索一步
if (backward(..., d, snake)) return true
}
return false
}
这种实现方式极其高效,不仅保证了找到的是最优路径,而且内存占用非常小,这对于移动设备上的 UI 框架至关重要。
Differ 的方法是在哪里被调用的?
答案是在 MyersDiff.kt 的 applyDiff 方法中。这个方法遍历计算出的差异路径,根据路径的走向决定是执行插入、删除还是保持不变。
// MyersDiff.kt
private fun applyDiff(diagonals: IntStack, callback: DiffCallback) {
// ... 遍历路径 ...
while (i < diagonals.size) {
// ... 计算坐标 ...
// 如果 posX < startX,说明有元素被删除了
while (posX < startX) {
callback.remove(posY, posX) // 调用 Differ.remove
posX++
}
// 如果 posY < startY,说明有元素被插入了
while (posY < startY) {
callback.insert(posY) // 调用 Differ.insert
posY++
}
// 对角线部分,说明元素相同(或可复用)
while (len-- > 0) {
callback.same(posX, posY) // 调用 Differ.same
posX++
posY++
}
}
}
4、1、2 具体案例分析
让我们通过几个具体例子来理解 structuralUpdate 的执行流程。
案例 1、完全相同 (ActionReuse)
场景:Modifier链没有变化。
// 旧 Modifier
Modifier.padding(10.dp).background(Color.Red)
// 新 Modifier(完全相同)
Modifier.padding(10.dp).background(Color.Red)
执行流程:
- 快速路径:
after.size == beforeSize(2 == 2),进入快速路径。 - 线性比对:
- 第 0 项:
PaddingElement(10.dp)vsPaddingElement(10.dp)actionForModifiers返回ActionReuse(因为prev == next)- 什么都不做,直接复用
PaddingNode
- 第 1 项:
BackgroundElement(Red)vsBackgroundElement(Red)actionForModifiers返回ActionReuse- 什么都不做,直接复用
BackgroundNode
- 第 0 项:
- 结果:所有 Node 都被复用,开销为 0。
案例 2、内容变化 (ActionUpdate)
场景:Modifier链结构不变,但某个 Modifier 的参数变了。
// 旧 Modifier
Modifier.padding(10.dp).background(Color.Red)
// 新 Modifier(padding 从 10 改为 20)
Modifier.padding(20.dp).background(Color.Red)
执行流程:
- 快速路径:
after.size == beforeSize(2 == 2),进入快速路径。 - 线性比对:
- 第 0 项:
PaddingElement(10.dp)vsPaddingElement(20.dp)prev == next返回false(padding 不同)areObjectsOfSameType返回true(都是PaddingElement)actionForModifiers返回ActionUpdate- 调用
updateNode(prev, next, node)更新PaddingNode PaddingNode的padding属性被更新为20.dp- 触发重新测量和布局
- 第 1 项:
BackgroundElement(Red)vsBackgroundElement(Red)actionForModifiers返回ActionReuse- 什么都不做,直接复用
BackgroundNode
- 第 0 项:
- 结果:
PaddingNode被更新,BackgroundNode被复用。开销极低(只更新一个属性)。
案例 3、类型变化 (ActionReplace)
场景:Modifier链结构发生变化,某个 Modifier 被替换为不同类型的 Modifier。
// 旧 Modifier
Modifier.padding(10.dp).background(Color.Red)
// 新 Modifier(background 被替换为 border)
Modifier.padding(10.dp).border(2.dp, Color.Black)
执行流程:
- 快速路径:
after.size == beforeSize(2 == 2),进入快速路径。 - 线性比对:
- 第 0 项:
PaddingElement(10.dp)vsPaddingElement(10.dp)actionForModifiers返回ActionReuse- 什么都不做,直接复用
PaddingNode
- 第 1 项:
BackgroundElement(Red)vsBorderElement(2.dp, Black)prev == next返回falseareObjectsOfSameType返回false(类型不同)actionForModifiers返回ActionReplace- 快速路径中断,进入
structuralUpdate
- 第 0 项:
- 结构更新:
offset = 1(从第 1 项开始 diff)before = [BackgroundElement(Red)]after = [BorderElement(2.dp, Black)]- Myers Diff 算法计算出操作序列:
[REPLACE] - 执行
REPLACE操作:- 调用
remove移除BackgroundNode - 调用
insert插入BorderNode
- 调用
- 结果:
PaddingNode被复用,BackgroundNode被删除,BorderNode被创建。
案例 4、插入新 Modifier (INSERT)
场景:在 Modifier 链中间插入一个新的 Modifier。
// 旧 Modifier
Modifier.padding(10.dp).background(Color.Red)
// 新 Modifier(在中间插入 offset)
Modifier.padding(10.dp).offset(5.dp).background(Color.Red)
执行流程:
- 快速路径:
after.size != beforeSize(3 != 2),跳过快速路径。 - 结构更新:
offset = 0(从头开始 diff)before = [PaddingElement(10.dp), BackgroundElement(Red)]after = [PaddingElement(10.dp), OffsetElement(5.dp), BackgroundElement(Red)]- Myers Diff 算法计算出操作序列:
[SAME, INSERT, SAME] - 执行操作:
- SAME:
PaddingElement(10.dp)vsPaddingElement(10.dp),复用PaddingNode - INSERT: 插入
OffsetElement(5.dp),创建OffsetNode - SAME:
BackgroundElement(Red)vsBackgroundElement(Red),复用BackgroundNode
- SAME:
- 结果:
PaddingNode和BackgroundNode被复用,OffsetNode被创建。
案例 5、删除 Modifier (DELETE)
场景:从 Modifier 链中删除一个 Modifier。
// 旧 Modifier
Modifier.padding(10.dp).offset(5.dp).background(Color.Red)
// 新 Modifier(删除 offset)
Modifier.padding(10.dp).background(Color.Red)
执行流程:
- 快速路径:
after.size != beforeSize(2 != 3),跳过快速路径。 - 结构更新:
offset = 0(从头开始 diff)before = [PaddingElement(10.dp), OffsetElement(5.dp), BackgroundElement(Red)]after = [PaddingElement(10.dp), BackgroundElement(Red)]- Myers Diff 算法计算出操作序列:
[SAME, DELETE, SAME] - 执行操作:
- SAME:
PaddingElement(10.dp)vsPaddingElement(10.dp),复用PaddingNode - DELETE: 删除
OffsetElement(5.dp),移除OffsetNode - SAME:
BackgroundElement(Red)vsBackgroundElement(Red),复用BackgroundNode
- SAME:
- 结果:
PaddingNode和BackgroundNode被复用,OffsetNode被删除。
案例 6、复杂场景 (插入 + 删除 + 替换)
场景:Modifier 链发生复杂变化。
// 旧 Modifier
Modifier.padding(10.dp).background(Color.Red).clickable { }
// 新 Modifier
Modifier.padding(20.dp).offset(5.dp).background(Color.Blue)
执行流程:
- 快速路径:
after.size != beforeSize(3 != 3),但快速路径会在第 0 项失败。 - 线性比对:
- 第 0 项:
PaddingElement(10.dp)vsPaddingElement(20.dp)actionForModifiers返回ActionUpdate- 更新
PaddingNode
- 第 1 项:
BackgroundElement(Red)vsOffsetElement(5.dp)actionForModifiers返回ActionReplace- 快速路径中断,进入
structuralUpdate
- 第 0 项:
- 结构更新:
offset = 1(从第 1 项开始 diff)before = [BackgroundElement(Red), ClickableElement]after = [OffsetElement(5.dp), BackgroundElement(Blue)]- Myers Diff 算法计算出操作序列:
[INSERT, REPLACE, DELETE] - 执行操作:
- INSERT: 插入
OffsetElement(5.dp),创建OffsetNode - REPLACE:
BackgroundElement(Red)vsBackgroundElement(Blue),更新BackgroundNode - DELETE: 删除
ClickableElement,移除ClickableNode
- INSERT: 插入
- 结果:
PaddingNode被更新,OffsetNode被创建,BackgroundNode被更新,ClickableNode被删除。
五、总结
- 修饰符的链式调用实际是生成了一棵树,对象不可变,保证线程安全。
- 将树形结构展平成双向链表。
- 重组的时候通过差异算法执行创建、更新、删除。