Compose 布局与绘制:LayoutNode、DisplayList 与 GraphicsLayer

Composable 在 Android 上通过 View 系统接入窗口绘制流程。AndroidComposeView 是 View 树中真实存在的 ViewGroup,接收 ViewRootImpl 驱动的测量、布局和绘制回调,并管理 Compose 的 LayoutNode 树。

Compose 的界面更新包含 Composition、Layout 和 Draw 三个阶段。本文聚焦后两个阶段:LayoutNode 如何完成布局,以及绘制命令如何录制到 DisplayList。

Compose 与传统 View 的布局对比

两者都要完成测量、放置和绘制,差异主要体现在节点模型、约束表达和失效粒度。

对比项 传统 View Compose
布局节点 ViewViewGroup LayoutNode
父级约束 MeasureSpec Constraints
子节点测量结果 measuredWidth / measuredHeight Placeable
确定子节点位置 layout()onLayout() place()placeRelative()
常规 DisplayList 归属 每个 View 通常有自己的 RenderNode 多个 LayoutNode 默认共享同一个 RenderNode

Compose 的 Layout 阶段分为 Measure 和 Placement。父节点向子节点传递 Constraints,根据子节点返回的 Placeable 确定自身尺寸,再放置子节点:

kotlin 复制代码
Layout(content = content) { measurables, constraints ->
    val placeable = measurables.first().measure(constraints)

    layout(placeable.width, placeable.height) {
        placeable.placeRelative(0, 0)
    }
}
  • measure() 按约束测量子节点,返回 Placeable
  • layout(width, height) 确定当前节点的尺寸,同时声明子节点怎么放。
  • placeRelative() 确定子节点在当前节点坐标系里的位置。当前节点自己的位置由父节点决定。

Measure 和 Placement 分别跟踪状态读取。状态只在 Placement 中读取时,变化后通常可以跳过重新测量。

AndroidComposeView 如何连接两套布局系统

AndroidComposeView 是 Android View 树中的 ViewGroup,也是 Compose LayoutNode 树的 Owner

text 复制代码
ViewRootImpl
└── Android View 树
    └── AndroidComposeView
        └── Compose LayoutNode 树

它在 onMeasure() 中将 MeasureSpec 转换为 Constraints,测量根 LayoutNode 并向 View 系统报告尺寸;在 onLayout() 中完成待处理的 Measure 和 Placement;在 dispatchDraw() 中补齐未完成的布局任务,再调用根 LayoutNode 的绘制入口。

Compose 的 DisplayList 录制

在 Android 的硬件加速路径下,传统 View 与 Compose 的绘制命令最终都由 HWUI 处理。传统 View 通常由每个 View 对应的 RenderNode 保存 DisplayList;Compose 的 LayoutNode 与 RenderNode 不一一对应,多个 LayoutNode 可以共享一个 RenderNode。下文统一按这条常见路径描述,旧版本的兼容实现不影响整体关系。

text 复制代码
ViewRootImpl 执行 View 树绘制
        ↓
AndroidComposeView.dispatchDraw(canvas)
        ↓
root LayoutNode.draw(canvas)
        ↓
遍历 DrawModifier 和子 LayoutNode
        ↓
绘制命令写入当前 RenderNode 的 DisplayList

LayoutNode 是 Compose 的逻辑节点,默认不创建独立 RenderNode。根 LayoutNode 对应根 RenderNode;其他节点默认录制到根 RenderNode,如果外层存在 graphicsLayer,则录制到该 GraphicsLayer 对应的 RenderNode。因此,多个 LayoutNode 可以共享同一个 DisplayList。

text 复制代码
根 RenderNode
├── DisplayList
|   ├── LayoutNode A 的绘制命令
|   ├── LayoutNode B 的绘制命令
|   └── LayoutNode C 的绘制命令
└── 属性
    ├── translation
    └── ...

DisplayList 按顺序保存绘制命令,但 LayoutNode 树仍负责测量、放置和绘制遍历。树状关系通过绘制顺序、坐标变换和裁剪命令体现在 DisplayList 中。

GraphicsLayer:按需创建独立 RenderNode

GraphicsLayer 是 Compose 对独立图层的抽象,在 Android 的常见硬件加速路径下由 RenderNode 承载。它保存独立的 DisplayList,以及作用于整组内容的平移、缩放、旋转、透明度和裁剪等属性。

Modifier.graphicsLayer() 会为所包裹的内容创建独立 GraphicsLayer,也就是独立 RenderNode。父 RenderNode 的 DisplayList 不再直接包含内部命令,而是记录对子 RenderNode 的引用:

text 复制代码
父 RenderNode
└── DisplayList
    ├── 绘制节点 A
    ├── 绘制子 RenderNode B
    └── 绘制节点 C

子 RenderNode B(GraphicsLayer)
├── DisplayList:graphicsLayer 包裹的绘制命令
└── 图层属性
    ├── translation
    ├── scale
    ├── rotation
    ├── alpha
    └── clip

LayoutNode 自身没有 GraphicsLayer 时,绘制命令继续写入当前 RenderNode:默认是根 RenderNode;如果外层存在 GraphicsLayer,则是外层 GraphicsLayer 对应的 RenderNode。内容或局部效果变化需要更新这个 RenderNode 的 DisplayList。创建独立 GraphicsLayer 后,内容与 RenderNode 属性可以分开更新:

text 复制代码
内容变化
→ 重新录制子 RenderNode 的 DisplayList

translation、scale、rotation、alpha 等属性变化
→ 更新子 RenderNode 的属性
→ 不重新录制内容

graphicsLayer 只影响 Draw 阶段,不改变 Measure 和 Placement 确定的尺寸与位置。

GraphicsLayer 的创建场景

alpharotatescaleclipshadowblur 等 Modifier 也会在内部使用 GraphicsLayer。

GraphicsLayer 通常用于以下情况:

  • 内容需要独立失效和重录。
  • 平移、缩放、旋转或透明度频繁变化。
  • 需要阴影、图层裁剪、混合或 RenderEffect

图层属性由 State 或动画驱动时,可以使用 lambda 版本:

kotlin 复制代码
Modifier.graphicsLayer {
    translationX = offset.value
    alpha = opacity.value
}

状态读取发生在图层属性更新范围内,变化时可以只更新图层属性,跳过重组和重新布局。

Compose 默认让多个 LayoutNode 共享 RenderNode,仅在需要独立更新或图层效果时通过 GraphicsLayer 创建独立 RenderNode。共享可以减少 RenderNode 数量,但内容变化时可能需要重新录制较大的 DisplayList;独立 RenderNode 能缩小更新范围,同时会增加对象、遍历和合成成本。

总结

Compose 在 Android 上的布局与绘制链路如下:

text 复制代码
ViewRootImpl 驱动 AndroidComposeView
        ↓
AndroidComposeView 将 MeasureSpec 转换为 Constraints
        ↓
LayoutNode 树完成 Measure 和 Placement
        ↓
Draw 阶段遍历 LayoutNode 树
        ↓
绘制命令录制到根 RenderNode 或外层 GraphicsLayer 的 RenderNode
        ↓
graphicsLayer 为所包裹的内容创建独立 RenderNode
        ↓
ThreadedRenderer 将渲染任务交给 RenderThread

LayoutNode 树负责布局和绘制遍历,RenderNode 保存 DisplayList,GraphicsLayer 用于创建独立 RenderNode 并设置图层属性。

参考资料与源码

相关推荐
淡淡的香烟1 小时前
Android常用adb命令
android·adb
艾莉丝努力练剑4 小时前
【MYSQL】MYSQL学习的一大重点:事务(上)- 原子性与持久性
android·数据库·学习·mysql·面试
plainGeekDev15 小时前
工厂模式 → Hilt
android·java·kotlin
雨白15 小时前
深入理解 Kotlin 协程 (十):引而不发,探秘 Flow 冷流机制与异常透明性
android·kotlin
plainGeekDev15 小时前
Application 单例 → Hilt Singleton
android·java·kotlin
程序员码歌16 小时前
我全程用 AI开发了一款微信小游戏,上线了
android·前端·游戏开发
黄林晴17 小时前
你敢信吗?同样是跨端,KMP 跟 RN 居然差这么多!
android·前端
zbmwa18 小时前
jetpack compose 副作用 produceState
android
hunterandroid19 小时前
Android 后台任务可靠性排查:从 WorkManager 观测到失败重试闭环
android·前端