1.1 概述
应用层视图绘制阶段运行在 UI Thread(主线程) 上,是整个图形渲染流水线的起点。该阶段的核心任务是将应用开发者编写的 View 树遍历并转换为 GPU 可理解的 DisplayList (显示列表),然后通过 RenderProxy 将 DisplayList 提交给 RenderThread 进行异步渲染。
1.2 调用链总览
scss
Choreographer.doFrame()
└── ViewRootImpl.doTraversal()
└── ViewRootImpl.performTraversals()
├── performMeasure() // 测量阶段
├── performLayout() // 布局阶段
└── performDraw() // 绘制阶段
└── ThreadedRenderer.draw()
├── updateRootDisplayList() // 构建 DisplayList
└── syncAndDrawFrame() // 提交到 RenderThread
1.3 关键源码分析
1.3.1 Choreographer 驱动帧循环
Choreographer 是 Android 帧循环的调度中心,它监听 VSync 信号,在每次垂直同步到来时回调 doFrame() 方法。
源码路径 : frameworks/base/core/java/android/view/Choreographer.java
ViewRootImpl 通过 scheduleTraversals() 向 Choreographer 注册一个 TraversalRunnable,该 Runnable 在下一个 VSync 到来时被调用:
csharp
private void performTraversals(long frameTimeNanos) {
mLastPerformTraversalsSkipDrawReason = null;
// cache mView since it is used so much below below...
final View host = mView;
if (DBG) {
System.out.println("======================================");
System.out.println("performTraversals");
host.debug();
}
if (host == null || !mAdded) {
mLastPerformTraversalsSkipDrawReason = host == null ? "no_host" : "not_added";
return;
}
1.3.2 performTraversals --- 三阶段调度
performTraversals() 是 View 树遍历的核心调度方法,依次执行三个子阶段:
-
performMeasure --- 自顶向下遍历 View 树,计算每个 View 的期望尺寸
-
performLayout --- 自顶向下遍历 View 树,确定每个 View 的位置和大小
-
performDraw --- 触发实际的绘制操作
在 performDraw 阶段,关键逻辑如下(位于 ViewRootImpl.java 约第 8456 行):
kotlin
mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
这里调用了 ThreadedRenderer.draw(),标志着从 View 系统进入 HWUI 渲染引擎。
1.3.3 ThreadedRenderer.draw --- 构建 DisplayList 并提交
源码路径 : frameworks/base/core/java/android/view/ThreadedRenderer.java
ini
void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
final Choreographer choreographer = attachInfo.mViewRootImpl.mChoreographer;
attachInfo.mViewRootImpl.mViewFrameInfo.markDrawStart();
updateRootDisplayList(view, callbacks);
该方法包含两个核心步骤:
步骤 1: updateRootDisplayList --- 构建 DisplayList
updateRootDisplayList() 负责遍历 View 树,将每个 View 的绘制操作记录到 RenderNode 的 DisplayList 中。其核心流程如下:
-
获取或创建根
RenderNode的SkiaRecordingCanvas -
调用
View.draw(canvas)触发 View 树的递归绘制 -
每个 View 的
onDraw()方法将绘制指令(drawRect、drawBitmap、drawText 等)记录到 Canvas 中 -
SkiaRecordingCanvas将这些指令编码为 Skia 的绘制操作,存储在SkiaDisplayList中
关键类:
-
RenderNode--- 对应 View 树中的一个节点,持有 DisplayList 和渲染属性 -
SkiaRecordingCanvas--- 继承自SkiaCanvas,负责将绘制 API 调用记录为 DisplayList
步骤 2: syncAndDrawFrame --- 提交到 RenderThread
ini
syncResult = syncAndDrawFrame(frameInfo);
syncAndDrawFrame() 是 JNI 调用,进入 Native 层的 RenderProxy::syncAndDrawFrame()。该方法将构建好的 DisplayList 同步到 RenderThread,并触发异步渲染。
1.3.4 SkiaRecordingCanvas --- DisplayList 的录制器
源码路径 : frameworks/base/libs/hwui/pipeline/skia/SkiaRecordingCanvas.h
arduino
class SkiaRecordingCanvas : public SkiaCanvas {
public:
explicit SkiaRecordingCanvas(uirenderer::RenderNode* renderNode, int width, int height) {
initDisplayList(renderNode, width, height);
}
SkiaRecordingCanvas 的核心机制:
-
initDisplayList --- 初始化或复用
SkiaDisplayList,并绑定RecordingCanvas(Skia 的SkLiteRecorder) -
录制过程 --- 所有
drawBitmap()、drawRect()、drawRenderNode()等调用被记录为 Skia 的绘制操作(SkDrawable子类) -
finishRecording --- 完成录制,将 DisplayList 关联到 RenderNode
arduino
void SkiaRecordingCanvas::initDisplayList(uirenderer::RenderNode* renderNode, int width,
int height) {
mCurrentBarrier = nullptr;
LOG_FATAL_IF(mDisplayList.get() != nullptr);
if (renderNode) {
mDisplayList = renderNode->detachAvailableList();
}
if (!mDisplayList) {
mDisplayList.reset(new SkiaDisplayList());
}
mDisplayList->attachRecorder(&mRecorder, SkIRect::MakeWH(width, height));
SkiaCanvas::reset(&mRecorder);
关键点:
-
renderNode->detachAvailableList()--- 尝试复用上一帧的 DisplayList,避免重复分配 -
mDisplayList->attachRecorder()--- 将 Skia 的RecordingCanvas绑定到 DisplayList -
SkiaCanvas::reset(&mRecorder)--- 将 Canvas 的绘制目标设置为 recorder
1.3.5 RenderNode --- 视图节点的渲染表示
源码路径 : frameworks/base/libs/hwui/RenderNode.h
RenderNode 是 View 在渲染层的对应表示,每个 View 对应一个 RenderNode。其关键职责:
-
持有
SkiaDisplayList(录制好的绘制指令集合) -
管理渲染属性(位置、变换、透明度、裁剪区域等)
-
维护子 RenderNode 列表(对应 View 树的层级结构)
-
支持 DisplayList 的缓存与复用
当 View 树中的某个 View 发生局部更新时,只有对应的 RenderNode 需要重新录制 DisplayList,其他节点可以复用上一帧的 DisplayList,这是 Android 渲染性能优化的核心机制之一。
1.4 DisplayList 的构建与缓存机制
1.4.1 构建流程
scss
View.draw(canvas)
└── View.onDraw(canvas) // 应用自定义绘制
└── View.dispatchDraw(canvas) // 绘制子 View
└── drawChild(canvas, child, ...)
└── child.draw(canvas) // 递归子 View
└── RenderNode.record(...) // 记录到 DisplayList
1.4.2 缓存复用
当 View 树没有发生变化时(通过 View.mPrivateFlags 中的 PFLAG_INVALIDATED 标志判断),RenderNode 可以直接复用上一帧的 DisplayList,跳过录制过程。这通过 renderNode->detachAvailableList() 实现:
-
如果 RenderNode 持有上一帧的 DisplayList,则 detach 并复用
-
否则创建新的
SkiaDisplayList
1.4.3 脏区域追踪
DamageAccumulator 负责追踪需要重绘的脏区域。在 CanvasContext::prepareTree() 中,脏区域信息被累积并传递给渲染管线,用于优化后续的渲染范围:
ini
info.damageAccumulator = &mDamageAccumulator;
info.colorArea = &mColorArea;
info.layerUpdateQueue = &mLayerUpdateQueue;
info.damageGenerationId = mDamageId++;
1.5 缓冲区流转起点
在应用层绘制阶段结束时,DisplayList 已经构建完成,但尚未产生任何 GraphicBuffer。真正的缓冲区操作发生在 RenderThread 阶段。应用层通过 syncAndDrawFrame() 将 DisplayList 的引用传递给 RenderThread,UI 线程随即可以继续处理下一帧的输入事件,实现 UI 线程与渲染线程的并行执行。
1.6 关键类关系图
scss
ViewRootImpl
└── mAttachInfo.mThreadedRenderer (ThreadedRenderer)
└── mRootNode (RenderNode) // 根节点
└── mDisplayList (SkiaDisplayList) // 录制的绘制指令
└── mRecorder (RecordingCanvas) // Skia 录制器
└── drawOps... // 具体的绘制操作
1.7 总结
应用层视图绘制阶段的核心产出是 DisplayList,它是对 View 树绘制操作的延迟记录。通过将绘制指令录制为 DisplayList,Android 实现了:
-
UI 线程与渲染线程的解耦 --- UI 线程只需构建 DisplayList,无需等待 GPU 完成
-
增量更新 --- 只有脏区域对应的 RenderNode 需要重新录制
-
DisplayList 复用 --- 未发生变化的节点可以直接复用上一帧的 DisplayList
-
跨进程传递 --- DisplayList 最终通过
RenderProxy跨线程传递给 RenderThread