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 并设置图层属性。

参考资料与源码

相关推荐
三8441 天前
PHP 绕过污点追踪构造 Webshell:反序列化引用复用与 Trait 可变函数 RCE
android
石头猫灯1 天前
WordPress wp2shell漏洞断点逐过程到注入点
android·学习
Android打工仔1 天前
Kotlin 协程源码解析(五):BaseContinuationImpl.resumeWith() —— Continuation 链是如何被展开的?
android·kotlin
存在morning1 天前
【PySpark 学习笔记 三】DataFrame API 入门
android·java·数据库
YSoup1 天前
2026 安卓面试助手APP(安卓八股、题库)
android·面试·职场和发展
xcyxiner1 天前
flutter 运行到模拟器上
android·前端·flutter
Android-Flutter1 天前
android Glide 源码流程分析
android·kotlin
Android-Flutter1 天前
android Glide 使用详解
android·kotlin
恋猫de小郭1 天前
Dart 3.13 的到底改了什么?为什么很重要?有什么坑?
android·前端·flutter
HZZD_HZZD1 天前
3T-UEM 数据建模实战:三维立方体 + LAM/ELA 双归因怎么落地?
android