在硬件加速窗口的常规 View 绘制路径中,主线程的 Draw 阶段主要记录绘制命令,而不是直接生成最终像素。View 的绘制操作被记录为 DisplayList,并由 RenderNode 持有;位移、缩放和透明度等渲染属性则单独保存在 RenderNode 中。

Draw 阶段:记录绘制命令
View.draw() 和 onDraw() 在主线程执行。硬件加速路径下,传入的 Canvas 是 RecordingCanvas。它会记录本次录制阶段中发生的 Canvas 绘制与状态操作,包括 drawRect()、drawText()、drawBitmap()、裁剪和矩阵变换等调用及其参数,并将它们写入当前 RenderNode 的 DisplayList。这一阶段只生成绘制记录,不完成最终栅格化。
录制完成后,渲染节点信息会同步给 RenderThread,HWUI 再通过 Skia 和图形后端处理这些命令。因此,一帧的绘制可简化为两部分:主线程生成绘制记录,RenderThread 和 GPU 执行记录并生成像素。两部分可以流水线方式处理不同帧。
录制由 RenderNode 发起,过程可概括为三步:
java
RecordingCanvas canvas = renderNode.beginRecording(width, height);
draw(canvas);
renderNode.endRecording();
beginRecording() 获取本次录制使用的 RecordingCanvas,draw() 写入绘制操作,endRecording() 结束录制。这三步生成的结果就是后文介绍的 DisplayList。
DisplayList:可复用的绘制记录
DisplayList 是一次录制的结果,其中包含 Canvas 绘制操作、裁剪、矩阵变换和状态保存等命令。简化后如下:
text
DisplayList
├── save()
├── clipRect(...)
├── drawRect(...)
├── drawText(...)
├── drawBitmap(...)
├── drawRenderNode(child)
└── restore()
DisplayList 中不包含最终像素。例如,记录一条 drawText() 命令并不等于文字已经显示,后续仍需要 RenderThread 和 GPU 执行相关操作。
因此,复用 DisplayList 可以避免主线程重新执行相同的绘制记录工作,但不会消除每帧的渲染执行和合成成本。
RenderNode:View 的渲染节点
每个 View 内部都持有一个 RenderNode。RenderNode 主要管理两类数据:
text
RenderNode
├── DisplayList:绘制内容
│ ├── drawRect(...)
│ ├── drawText(...)
│ ├── drawBitmap(...)
│ └── drawRenderNode(child)
│
└── 渲染属性
├── alpha
├── translationX / translationY
├── scaleX / scaleY
├── rotation
├── elevation
├── clip
└── bounds
DisplayList 描述"画什么",渲染属性描述绘制结果"如何放置和合成"。这种分离使属性变化不必然导致绘制内容重新录制。
RenderNode 并非只由 View 系统内部使用。Android 也提供公开的 RenderNode API,应用可以自行创建节点并录制内容。因此,常规 View 绘制中的 RenderNode 结构与 View 树大体对应,但不是严格的一一映射。
RenderNode 树如何组织绘制内容
完整界面的绘制内容由 RenderNode 树组织。以下面的 View 树为例:
text
LinearLayout
├── TextView
└── Button
对应的 RenderNode 关系可简化为:
text
LinearLayout RenderNode
└── DisplayList
├── drawBackground()
├── drawRenderNode(TextView RenderNode)
└── drawRenderNode(Button RenderNode)
TextView RenderNode
├── properties
│ ├── bounds
│ ├── alpha
│ └── translation
└── DisplayList
├── drawBackground()
└── drawText()
父节点的 DisplayList 不展开子节点的绘制细节,而是通过 drawRenderNode() 引用子 RenderNode。子节点内容变化时,可以只重录子节点的 DisplayList;父节点保留对同一 RenderNode 的引用,不需要仅因子节点内容变化而重录。
DisplayList 的复用以 RenderNode 为粒度。系统判断各节点的绘制内容是否失效:失效的节点重新录制,其余节点继续使用现有记录。
界面变化时的三类更新
界面变化可按更新对象分为三类。
1. 渲染属性变化
translationX、translationY、alpha、scaleX、scaleY 和 rotation 等属性单独保存在 RenderNode 中。修改这些属性通常只需更新属性值,不会因此重新执行 onDraw(),也不需要重走 Measure 和 Layout。
以 setTranslationX() 为例,绘制内容未变,变化的只是位置。下一帧可以继续使用原有 DisplayList,并应用新的位移属性。这也是对这些属性做动画通常成本较低的原因。
2. 绘制内容变化
文字、背景或自定义绘制数据变化时,需要通过 invalidate() 将绘制内容标记为失效。后续 Traversal 进入 Draw 阶段时,对应 RenderNode 的 DisplayList 会重新录制。
invalidate() 不会立即执行 onDraw(),它只更新失效状态并请求后续帧。同一帧内的多次调用通常会合并处理;如果每帧都修改内容并调用 invalidate(),DisplayList 也会随之频繁重录。
3. 布局或层级变化
修改宽高、约束等布局参数并调用 requestLayout(),会重新触发 Measure 和 Layout。如果尺寸或绘制内容同时改变,还可能需要重新录制 DisplayList。
添加、删除子 View,或者改变子 View 的绘制顺序,会改变父节点对子 RenderNode 的引用关系,因此需要更新父 RenderNode 的 DisplayList。
在常规 View 绘制中,属性更新通常不触发布局和重录;内容更新需要重录;布局更新还会增加 Measure 和 Layout 工作。自定义 View、特殊 Drawable 和 Framework 内部节点的具体路径可能不同。
从 RenderNode 区分两类绘制成本
RenderNode 和 DisplayList 将主线程的录制成本与后续的执行成本分开。排查绘制性能时,可以先判断问题位于哪一侧:
text
主线程录制成本
onDraw 中的计算与对象分配
invalidate 频率
需要记录的 Canvas 操作数量
RenderThread / GPU 执行成本
DisplayList 的长度与复杂度
路径、文字和位图处理
纹理上传、像素填充、混合与过度绘制
如果每帧都调用 invalidate(),复杂 Path 计算、大量对象创建和密集 Canvas 调用会反复占用主线程。此时应关注内容更新频率和录制逻辑。
如果 DisplayList 持续复用,onDraw() 没有重新执行,每帧仍然需要执行并合成已记录的绘制命令。DisplayList 过于复杂、纹理上传量大或透明混合范围过大时,成本可能主要落在 RenderThread 或 GPU 侧。
扩展:硬件加速窗口中的软件 Layer
部分 Canvas 操作在特定 Android 版本或硬件加速实现中不受支持,可能出现内容缺失、绘制错误或异常。应优先改用受支持的绘制方式;如果没有合适的替代方案,可以将受影响的最小范围 View 设为软件 Layer:
java
view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
这不会关闭整个窗口的硬件加速。以 AOSP Android 11 中 View.updateDisplayListIfDirty() 的简化逻辑为例:
java
RecordingCanvas canvas = renderNode.beginRecording(width, height);
if (layerType == LAYER_TYPE_SOFTWARE) {
buildDrawingCache(true);
Bitmap cache = getDrawingCache(true);
if (cache != null) {
canvas.drawBitmap(cache, 0, 0, mLayerPaint);
}
} else {
draw(canvas);
}
renderNode.endRecording();
beginRecording() 和 endRecording() 仍然存在,RenderNode 和 DisplayList 也会继续使用。普通路径直接录制 draw(canvas) 产生的绘制命令;软件 Layer 路径先由 CPU 将内容绘制到 Bitmap,再录制一条 drawBitmap() 命令,该位图随后作为纹理参与硬件合成。具体实现随 Android 版本而变,上述代码主要用于说明这两条路径的差异。
软件 Layer 会增加 Bitmap 内存开销;缓存失效后,还需要重新执行 CPU 栅格化和纹理上传。内容更新越频繁,这部分成本越高。因此,软件 Layer 更适合作为局部兼容手段;是否用于性能优化,需要根据具体场景测量。
各 Android 版本对 Canvas 操作的支持情况,可参考官方硬件加速文档。
总结
RenderNode 与 DisplayList 的核心关系可概括为三点:
- DisplayList 保存绘制内容,RenderNode 单独保存渲染属性。
- 内容失效时需要重录 DisplayList;仅修改渲染属性时,通常可以复用原有记录。
- DisplayList 复用节省的是主线程重录成本,不会消除 RenderThread 和 GPU 的执行成本。
排查绘制性能时,可以先确认 DisplayList 是否重录,再分别检查主线程录制与 RenderThread / GPU 执行两侧的成本。