RenderNode 与 DisplayList:Android 硬件加速的绘制记录与复用

3 阅读7分钟

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

image.png

Draw 阶段:记录绘制命令

View.draw()onDraw() 在主线程执行。硬件加速路径下,传入的 Canvas 是 RecordingCanvas。它会记录本次录制阶段中发生的 Canvas 绘制与状态操作,包括 drawRect()drawText()drawBitmap()、裁剪和矩阵变换等调用及其参数,并将它们写入当前 RenderNode 的 DisplayList。这一阶段只生成绘制记录,不完成最终栅格化。

录制完成后,渲染节点信息会同步给 RenderThread,HWUI 再通过 Skia 和图形后端处理这些命令。因此,一帧的绘制可简化为两部分:主线程生成绘制记录,RenderThread 和 GPU 执行记录并生成像素。两部分可以流水线方式处理不同帧。

录制由 RenderNode 发起,过程可概括为三步:

RecordingCanvas canvas = renderNode.beginRecording(width, height);
draw(canvas);
renderNode.endRecording();

beginRecording() 获取本次录制使用的 RecordingCanvasdraw() 写入绘制操作,endRecording() 结束录制。这三步生成的结果就是后文介绍的 DisplayList。

DisplayList:可复用的绘制记录

DisplayList 是一次录制的结果,其中包含 Canvas 绘制操作、裁剪、矩阵变换和状态保存等命令。简化后如下:

DisplayList
├── save()
├── clipRect(...)
├── drawRect(...)
├── drawText(...)
├── drawBitmap(...)
├── drawRenderNode(child)
└── restore()

DisplayList 中不包含最终像素。例如,记录一条 drawText() 命令并不等于文字已经显示,后续仍需要 RenderThread 和 GPU 执行相关操作。

因此,复用 DisplayList 可以避免主线程重新执行相同的绘制记录工作,但不会消除每帧的渲染执行和合成成本。

RenderNode:View 的渲染节点

每个 View 内部都持有一个 RenderNode。RenderNode 主要管理两类数据:

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 树为例:

LinearLayout
├── TextView
└── Button

对应的 RenderNode 关系可简化为:

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. 渲染属性变化

translationXtranslationYalphascaleXscaleYrotation 等属性单独保存在 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 将主线程的录制成本与后续的执行成本分开。排查绘制性能时,可以先判断问题位于哪一侧:

主线程录制成本
    onDraw 中的计算与对象分配
    invalidate 频率
    需要记录的 Canvas 操作数量

RenderThread / GPU 执行成本
    DisplayList 的长度与复杂度
    路径、文字和位图处理
    纹理上传、像素填充、混合与过度绘制

如果每帧都调用 invalidate(),复杂 Path 计算、大量对象创建和密集 Canvas 调用会反复占用主线程。此时应关注内容更新频率和录制逻辑。

如果 DisplayList 持续复用,onDraw() 没有重新执行,每帧仍然需要执行并合成已记录的绘制命令。DisplayList 过于复杂、纹理上传量大或透明混合范围过大时,成本可能主要落在 RenderThread 或 GPU 侧。

扩展:硬件加速窗口中的软件 Layer

部分 Canvas 操作在特定 Android 版本或硬件加速实现中不受支持,可能出现内容缺失、绘制错误或异常。应优先改用受支持的绘制方式;如果没有合适的替代方案,可以将受影响的最小范围 View 设为软件 Layer:

view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);

这不会关闭整个窗口的硬件加速。以 AOSP Android 11 中 View.updateDisplayListIfDirty() 的简化逻辑为例:

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 执行两侧的成本。

参考资料与源码