本文译自「The Different Node Types in Jetpack Compose」,原文链接www.grokkingandroid.com/the-differe...,由Wolfram Rittmeyer发布于2025年12月30日。

如果你仔细观察 Compose,你会发现它很奇怪。你会发现很多函数都没有返回值。然而,Android 文档却说:
可组合函数会生成 UI 层级结构。
这到底是什么意思呢?从函数式编程的角度来看,你那些看似无害的无状态可组合函数实际上会产生大量的副作用[1] 。其中就包括创建节点[2]。
有时你可能会创建一些可组合函数来自己创建节点。但更多时候,你会直接委托给其他可组合函数来创建节点。
那么,让我们来看看有哪些节点,它们的创建位置以及它们的用途:
- ComposeUiNode
- LayoutNode
- Modifier.Node
- SemanticsNode
ComposeUiNode
当你深入研究Layout可组合对象时,你会发现它调用了ReusableComposeNode[3]:
kotlin
@Composable
@UiComposable
inline fun Layout(modifier: Modifier = Modifier, measurePolicy: MeasurePolicy) {
val compositeKeyHash = currentCompositeKeyHash
val materialized = currentComposer.materialize(modifier)
val localMap = currentComposer.currentCompositionLocalMap
ReusableComposeNode<ComposeUiNode, Applier>(
factory = ComposeUiNode.Constructor,
update = {
set(measurePolicy, SetMeasurePolicy)
set(localMap, SetResolvedCompositionLocals)
set(materialized, SetModifier)
set(compositeKeyHash, SetCompositeKeyHash)
},
)
}
ReusableComposeNode本身并不是一个节点。 上面的代码中并没有构造函数调用,而是对另一个可组合对象的调用。第八行的工厂参数看似无关紧要,但实际上非常重要。因为最终会创建节点的就是这个函数。它会创建一个 ComposeUiNode。
ReusableComposeNode Composable 与 Compose 运行时紧密相关。这段代码告诉 Composer 应该启动一个 GroupKind.ReusableNode 类型的新组,然后代码会创建一个新节点,或者在重新组合时重用现有节点。最后,当使用 update 函数参数时,它会设置节点的内容:
kotlin
@Composable
inline fun <T : Any, reified E : Applier> ReusableComposeNode(
noinline factory: () -> T,
update: @DisallowComposableCalls Updater.() -> Unit
) {
if (currentComposer.applier !is E) invalidApplier()
currentComposer.startReusableNode()
if (currentComposer.inserting) {
currentComposer.createNode(factory)
} else {
currentComposer.useNode()
}
Updater(currentComposer).update()
currentComposer.endNode()
}
我们看到工厂被传递给了 createNode() 调用。此调用会安排在所有插入操作处理完毕后创建节点。Compose 运行时内部会延迟执行许多操作以实现优化。这里我们并不关心具体何时发生,只需知道它_将会_发生即可。当它发生时,工厂将被调用。
现在让我们更仔细地看一下工厂本身:ComposeUiNode.Constructor 看起来像是 ComposeUiNode 的构造函数。但事实并非如此。实际上,ComposeUiNode 是一个接口,它本身并不执行任何操作。它实际上是 LayoutNode 使用的基接口。至于这个接口和构造函数的用途,在接口声明上方的注释中已经给出:
kotlin
/** Interface extracted from LayoutNode to not mark the whole LayoutNode class as @PublishedApi. */
@PublishedApi
internal interface ComposeUiNode {
// ...
/** Object of pre-allocated lambdas used to make use with ComposeNode allocation-less. */
companion object {
val Constructor: () -> ComposeUiNode = LayoutNode.Constructor
val VirtualConstructor: () -> ComposeUiNode = { LayoutNode(isVirtual = true) }
// ...
}
}
所以 ComposeUiNode 只是 LayoutNode 的一个抽象。
布局节点(LayoutNode)
在上一节中,我们已经了解了 LayoutNode 的实际创建时间。基本上,每当调用 Layout 可组合组件时,都会创建 LayoutNode。LayoutNode 是树状结构中的元素,代表屏幕上的内容。它需要被测量,可以在其边界内放置子元素,也可以绘制内容。
每个 LayoutNode 都知道它的子元素和父元素。因此,LayoutNode 构成了一个节点树,代表了已发出的内容。需要明确的是:Compose 创建的树状结构与其他 UI 框架一样------但 Google 更倾向于使用一些更高级的命名方式。因此,每当你听到/读到有关可组合组件发出内容时,请将其理解为一个 LayoutNode 被创建并插入到树中。
LayoutNode 类实际上非常有趣,因此值得单独撰写一篇博文,我将在其中更详细地介绍它的一些方面。
这里有两点值得一提:
Applier是调用LayoutNode相应树状结构处理方法的实例。 -LayoutNode持有对Owner的引用。
由于 Applier 和 Owner 在 Compose 中都是非常重要的概念,我将在单独的博文中分别介绍它们。
LayoutNode 的用途
LayoutNode 构成 UI 树,并跟踪其父节点和子节点。UI 树本身的管理由 Applier 完成,我将在另一篇文章中介绍它。
另外,需要记住的一点是,LayoutNode 存储在 SlotTable[4] 中。因此,在重新组合时,如果运行时认为树的这一部分不需要更改,则可以重用现有的 LayoutNode。
LayoutNode 还会保存其修饰符(参见下一节),并委托这些修饰符来决定该 LayoutNode 需要多少空间(测量)、在屏幕上放置元素的位置(布局)以及最终在屏幕上显示什么内容(绘制)。
还有更多内容------但正如我提到的,那是另一篇文章的一部分。
Modifier.Node
修饰符在内部由 Modifier.Node 对象表示。根据文档,它是"为应用于 androidx.compose.ui.layout.Layout 的每个 Modifier.Element 创建的生命周期更长的对象"。
这个"生命周期更长"很有意思。基本上,只要 Modifier.Node 属于 LayoutNode 的修饰符链,它就会一直存在。 LayoutNode 持有一个 NodeChain 类型的对象,该对象内部维护着一个修饰符列表,并检查该列表是否发生更改,以及 Modifier.Node 的生命周期方法(例如 onAttach())是否被调用。
Modifier.Node 有许多现有的子类型,例如 LayoutModifierNode(见下文)或 DrawModifierNode。后者负责实际在屏幕上绘制内容,我计划在另一篇文章中详细介绍。
由于 LayoutNode 是 SlotTable 的一部分,因此附加到 LayoutNode 的 NodeChain 对象的 Modifier.Node 显然也是 SlotTable 的一部分。
特殊子类型:LayoutModifierNode
LayoutModifierNode(developer.android.com/reference/k... 会改变其包裹内容的测量和布局方式。"。因此,在测量和布局过程中,每当遍历 ModifierNode 链时,这些节点实际上都会开始进行测量。
因此,所有想要改变其子元素位置或对整个 Composable 元素大小产生任何影响的修饰符都需要实现 LayoutModifierNode 接口。例如,SizeNode 就是一个实现 LayoutModifierNode 接口的例子,它是 height()、width() 或 size() 等修饰符实际使用的节点。
SemanticsNode
当你想要向设备的*辅助功能服务[5]*传递一些信息时,你可以使用 Compose 中的 semantics 修饰符来告知系统要向用户传达哪些语义属性。
但是辅助功能服务并不了解 Compose。在其他平台上,这一点显而易见,但考虑到 Compose 向后兼容(而且它并非 Android 框架的组成部分),在 Android 上也是如此。
对于辅助功能,前面提到的 Owner 再次发挥作用。它持有一个 SemanticsOwner,该 SemanticsOwner 充当了与相应平台语义框架之间的桥梁。
为此,SemanticsOwner 维护着一个 SemanticNode 对象树,用于向辅助功能服务的用户描述屏幕内容。这也是我未来会更详细介绍的内容之一。
敬请期待更多关于 Compose 内部运作机制的见解。祝你编码愉快!
脚注
-
1\] 我在本段中使用"副作用"一词,是因为它在[计算机科学和函数式编程](https://link.juejin.cn?target=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FSide_effect_(computer_science) "https://en.wikipedia.org/wiki/Side_effect_(computer_science)")中由来已久。我这里指的并非官方 Compose 文档中提到的[副作用](https://link.juejin.cn?target=https%3A%2F%2Fdeveloper.android.com%2Fdevelop%2Fui%2Fcompose%2Fside-effects "https://developer.android.com/develop/ui/compose/side-effects")。Compose 文档至少在过去有时会提及副作用的这种用法------尽管实际上 Compose 函数并非纯函数。
-
3\] 实际上,布局可组合对象共有三个。但它们最终都会调用 `ReusableComposeNode`。其他变体的示例也与之非常相似。
-
5\] 设备上的辅助功能服务在 Android 系统中是 AccessibilityService 的实现,但在 iOS、桌面或 Web 端则有所不同,因此我在本文中使用"辅助功能服务"这个通用术语。
保护原创,请勿转载!