Jetpack Compose内部的不同节点类型

本文译自「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 可组合组件时,都会创建 LayoutNodeLayoutNode 是树状结构中的元素,代表屏幕上的内容。它需要被测量,可以在其边界内放置子元素,也可以绘制内容。

每个 LayoutNode 都知道它的子元素和父元素。因此,LayoutNode 构成了一个节点树,代表了已发出的内容。需要明确的是:Compose 创建的树状结构与其他 UI 框架一样------但 Google 更倾向于使用一些更高级的命名方式。因此,每当你听到/读到有关可组合组件发出内容时,请将其理解为一个 LayoutNode 被创建并插入到树中。

LayoutNode 类实际上非常有趣,因此值得单独撰写一篇博文,我将在其中更详细地介绍它的一些方面。

这里有两点值得一提:

  • Applier 是调用 LayoutNode 相应树状结构处理方法的实例。 - LayoutNode 持有对 Owner 的引用。

由于 ApplierOwner 在 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。后者负责实际在屏幕上绘制内容,我计划在另一篇文章中详细介绍。

由于 LayoutNodeSlotTable 的一部分,因此附加到 LayoutNodeNodeChain 对象的 Modifier.Node 显然也是 SlotTable 的一部分。

特殊子类型:LayoutModifierNode

LayoutModifierNodedeveloper.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 端则有所不同,因此我在本文中使用"辅助功能服务"这个通用术语。

保护原创,请勿转载!

相关推荐
Frank_HarmonyOS14 小时前
Android中四大组件之一的Activity的启动模式
android
似霰15 小时前
HIDL Hal 开发笔记7----简单 HIDL HAL 实现
android·framework·hal
用户20187928316718 小时前
📚 Android Settings系统:图书馆管理员的故事
android
青莲84318 小时前
Android 事件分发机制 - 事件流向详解
android·前端·面试
火柴就是我19 小时前
学习一些常用的混合模式之BlendMode. dst_atop
android·flutter
火柴就是我19 小时前
学习一些常用的混合模式之BlendMode. dstIn
android·flutter
ganshenml20 小时前
【Android】 开发四角版本全解析:AS、AGP、Gradle 与 JDK 的配套关系
android·java·开发语言
我命由我1234520 小时前
Kotlin 运算符 - == 运算符与 === 运算符
android·java·开发语言·java-ee·kotlin·android studio·android-studio
摘星编程21 小时前
【RAG+LLM实战指南】如何用检索增强生成破解AI幻觉难题?
android·人工智能