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 可组合组件时,都会创建 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 我在本段中使用"副作用"一词,是因为它在计算机科学和函数式编程中由来已久。我这里指的并非官方 Compose 文档中提到的副作用。Compose 文档至少在过去有时会提及副作用的这种用法------尽管实际上 Compose 函数并非纯函数。
  • 2 如果在 Compose Compiler 插件完成工作后查看 Composable,你会发现它不仅创建了节点,而且你的函数还会进行大量调用,从而改变应用程序的全局状态。但这些调用并不会实际绘制任何内容。它们最多只会创建一些绘制操作,这些操作会被记录下来,并在系统认为需要绘制时,于未来的某个时间点被调用。我将在以后的两篇文章中分别介绍绘制过程以及编译器对代码所做的更改。
  • 3 实际上,布局可组合对象共有三个。但它们最终都会调用 ReusableComposeNode。其他变体的示例也与之非常相似。
  • 4 本系列文章中我可能不会介绍 SlotTable。我推荐 Richard Leland 的这篇关于 compose 的文章(medium.com/androiddeve... Mohit Sarveiya 的关于 slotTable 的视频(codingwithmohit.com/),这些视频讲解了如何... slotTable。
  • 5 设备上的辅助功能服务在 Android 系统中是 AccessibilityService 的实现,但在 iOS、桌面或 Web 端则有所不同,因此我在本文中使用"辅助功能服务"这个通用术语。

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!

相关推荐
ZealSinger12 小时前
Boot4挂起函数丢traceId怎么修
spring boot·kotlin·协程·可观测性
素师良码17 小时前
第5篇:显示驱动必备调试工具浅析
android
维克兜率天17 小时前
【维克】配对交易的季节性:哪些品种适合长拿?
android·开发语言·笔记·python·算法·kotlin·量化
墨天梦18 小时前
B06_XML控件布局与ViewBinding
android·kotlin
晚风叙码18 小时前
MySQL 表的操作:从建表到删表,一篇讲清楚
android·mysql·oracle
事圆则缓19 小时前
Android 性能优化常见工具与使用场景:从症状到证据的排查指南
android·性能优化
avi911119 小时前
Unity 非后台,非自动构建,但统计报表 BuildReport ,构建自动化系统C#
android·webserver·ios打包·admin·c#后台·unity打包系统·unity自动构建
新鲜势力呀20 小时前
PHP 定时任务系统实战:从 Cron 混乱执行到任务调度中心 + Redis队列 + 失败重试
android·java·redis
YF021120 小时前
Android 16系统 如何修改 eth0 IP ?
android
其实防守也摸鱼20 小时前
渗透测试学习计划(全栈综合 · 零基础进阶)
android·数据库·学习·安全·oracle·自动化·学习方法