Composable 在 Android 上通过 View 系统接入窗口绘制流程。AndroidComposeView 是 View 树中真实存在的 ViewGroup,接收 ViewRootImpl 驱动的测量、布局和绘制回调,并管理 Compose 的 LayoutNode 树。
Compose 的界面更新包含 Composition、Layout 和 Draw 三个阶段。本文聚焦后两个阶段:LayoutNode 如何完成布局,以及绘制命令如何录制到 DisplayList。
Compose 与传统 View 的布局对比
两者都要完成测量、放置和绘制,差异主要体现在节点模型、约束表达和失效粒度。
| 对比项 | 传统 View | Compose |
|---|---|---|
| 布局节点 | View、ViewGroup |
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 的创建场景
alpha、rotate、scale、clip、shadow 和 blur 等 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 并设置图层属性。