hwui全称**HardwareAcceleratedRenderingEngineforUI,**hwui是一个基于GPU加速的2D图形引擎。HWUI的目标是提供高效、稳定、高质量的2D图形渲染能力,为Android系统的UI体验提供技术支持。
相关源码位于目录android/platform/framework/base/libs/hwui 文末有福利~
hwui的大部分代码以C++实现,Android平台对应到libhwui.so这个动态库.本文主要介绍一下hwui,简单梳理hwui绘制原理、绘制流程,不涉及过多细节。
概述
hwui简单讲,就是一个2D渲染引擎;Android各种View组件都是通过这个引擎实现的。
hwui设计
● hwui的2D绘制接口通过Canvas类提供,支持多种后端实现;skia opengles、skia vulkan、opengles(早期版本支持)等。
● DisplayList设计:hwui里有一个DisplayList设计,和OpenGL的DisplayList概念相似,都是用来打包渲染命令的;hwui的DisplayList打包的是skia渲染命令。 ● RenderNode设计:hwui一次渲染任务都是由一个个渲染节点RenderNode构成的,这些渲染节点组成树形结构;开始渲染时,从Root节点开始,以DFS的方式进行遍历处理。一个View至少有一个RenderNode。 ● 多线程模式:为了最大利用CPU性能,hwui把渲染分到了两个线程处理,即UI线程和RenderThread线程(简称RT线程)。UI线程负责整个VIew绘制逻辑,以及把Canvas的绘制命令打包成Skia的绘制命令存储到DisplayList;RT线程依次取出这些绘制命令并处理。 ● UI线程SkCanvas和RT线程SkCanvas区别:UI线程SkCanvas的实例是一个空壳,不会执行任何绘制操作,任务drawXXX函数都会调用到其派生类RecordingCanvas的onDrawXXX函数内部;RT线程SkCanvas的实例,通过SkSurface获取,和opengles后端绑定,会执行当前的绘制操作。
Canvas分析
hwui的绘制接口都是通过Canvas类向外部提供的。
传给View系统的都是Canvas的派生类RecordingCanvas的实例;RecordingCanvas对应到Native层就是SkiaRecordingCanvas。SkiaRecordingCanvas里面有两个关键的成员变量:RecordingCanvas和SkiaDisplayList。RecordingCanvas是SkCanvas的派生类。
Canvas一般都调用RenderNode的beginRecording函数创建。流程如下:
Canvas的draw函数分析
执行Canvas的draw call函数时,Canvas相关的绘制函数会调用到SkiaRecordingCanvas内的绘制函数;SkiaRecordingCanvas内的绘制函数会把绘制命令转化成Skia绘制命令,并调用RecordingCanvas内的绘制函数;RecordingCanvas内的绘制函数会调用DisplayListData内的绘制函数;DisplayListData内的绘制函数就会把绘制命令存起来。
struct DrawRect final : Op {
static const auto kType = Type::DrawRect;
DrawRect(const SkRect& rect, const SkPaint& paint) : rect(rect), paint(paint) {}
SkRect rect;
SkPaint paint;
void draw(SkCanvas* c, const SkMatrix&) const { c->drawRect(rect, paint); }
};
void SkiaCanvas::drawRect(float left, float top, float right, float bottom, const Paint& paint) {
if (CC_UNLIKELY(paint.nothingToDraw())) return;
applyLooper(&paint, [&](const SkPaint& p) {
mCanvas->drawRect({left, top, right, bottom}, p);
});
}
void SkCanvas::drawPaint(const SkPaint& paint) {
TRACE_EVENT0("skia", TRACE_FUNC);
this->onDrawPaint(paint);
}
void RecordingCanvas::onDrawRect(const SkRect& rect, const SkPaint& paint) {
fDL->drawRect(rect, paint);
}
void DisplayListData::drawRect(const SkRect& rect, const SkPaint& paint) {
this->push<DrawRect>(0, rect, paint);
}
hwui一次渲染任务都是由一个个渲染节点RenderNode构成的,这些渲染节点组成树形结构。开始渲染时,从Root节点开始,以DFS方式进行遍历处理。
RenderNode的渲染命令和数据存储在mDisplayList、mStagingDisplayList和mProperties、mStagingProperties。分析RenderNode就是分析mDisplayList、mStagingDisplayList和mProperties、mStagingProperties的设置逻辑、处理逻辑。
每个VIew持有一个RenderNode,同时HardwareRenderer持有一个RenderNode;HardwareRenderer持有的RenderNode为Root RenderNode,VIew持有的RenderNode为Sub RenderNode。HardwareRenderer的Root RenderNode在HardwareRenderer构造时被创建,VIew的RenderNode在VIew构造时被创建。
RenderNode的属性设置函数(如setTranslationX等),会把属性值设置到mStagingProperties里面,并把mDirtyPropertiesFields对应的bit位设置成1。
RenderNode的beginRecording函数会创建一个RecordingCanvas,并保存到mCurrentRecordingCanvas,然后把这个RecordingCanvas传到上层;上层通过这个Canvas执行的绘制指令,都会被存到该Canvas的DisplayList中。RenderNode的endRecording函数会把mCurrentRecordingCanvas中的mDisplayList保存到mStagingDisplayList。
mStagingDisplayList不仅存储着渲染指令,还存储子渲染节点。当通过RenderNode(假设A)生成的Canvas绘制其他RenderNode(假设B)时,drawRenderNode函数除了调用drawDrawable生成一个绘制命令保存起来,还会将被绘制的RenderNode保存到mStagingDisplayList的mChildNodes中;这个被绘制的RenderNode(B)就成了RenderNode(A)的子节点。
RenderNode的prepareTreeImpl函数会把mStagingProperties保存到mProperties、mStagingDisplayList保存到mDisplayList中,并递归调用mDisplayList的mChildNodes的prepareTreeImpl函数。
创建自己的RenderNode,并加入到View系统中
View & ViewGroup是Android View系统UI控件基类。通过重载View类的onDraw函数可以绘制出各种各样的控件。 View和RenderNode一样都是树形结构。树的分支节点是ViewGroup类型,叶子节点是View类型。每个View实例对应一个RenderNode实例mRenderNode;View的onDraw函数中的canvas参数,就是通过mRenderNode获取的。
View类图梳理
updateDisplayListIfDirty函数分析
updateDisplayListIfDirty函数触发View的绘制(此处说的绘制,是更新DisplayList);执行updateDisplayListIfDirty函数后,View就会绘制自己以及子View。draw函数、onDraw函数都是在此时触发执行的。下图是最顶层View的绘制流程。
渲染流程分析
初步了解了Canvas、RenderNode、View的设计后,再分析渲染流程就非常简单了。 渲染流程分为如下几个关键步骤:
IRenderPipeline::DrawResult SkiaOpenGLPipeline::draw(
const Frame& frame, const SkRect& screenDirty, const SkRect& dirty,
const LightGeometry& lightGeometry, LayerUpdateQueue* layerUpdateQueue,
const Rect& contentDrawBounds, bool opaque, const LightInfo& lightInfo,
const std::vector<sp<RenderNode>>& renderNodes, FrameInfoVisualizer* profiler) {
...
sk_sp<SkSurface> surface(SkSurface::MakeFromBackendRenderTarget(
mRenderThread.getGrContext(), backendRT, this->getSurfaceOrigin(), colorType,
mSurfaceColorSpace, &props));
...
renderFrame(*layerUpdateQueue, dirty, renderNodes, opaque, contentDrawBounds, surface,
SkMatrix::I());
...
{
ATRACE_NAME("flush commands");
surface->flushAndSubmit();
}
...
}
void SkiaPipeline::renderFrame(const LayerUpdateQueue& layers, const SkRect& clip,
const std::vector<sp<RenderNode>>& nodes, bool opaque,
const Rect& contentDrawBounds, sk_sp<SkSurface> surface,
const SkMatrix& preTransform) {
...
// Initialize the canvas for the current frame, that might be a recording canvas if SKP
// capture is enabled.
SkCanvas* canvas = tryCapture(surface.get(), nodes[0].get(), layers);
...
renderFrameImpl(clip, nodes, opaque, contentDrawBounds, canvas, preTransform);
...
}
当View组件内容出现更新,或者属性出现更新时,就会触发hwui的绘制。 View组件内容更新时,会执行View的requestLayout函数;View的requestLayout函数会调用parent的requestLayout函数,一直递归到ViewRootImpl。 ViewRootImpl的requestLayout函数会执行scheduleTraversals函数;scheduleTraversals函数申请Vsync信号;下个周期的Vsync信号到来,ViewRootImpl就会执行performTraversals函数;performTraversals函数内部就会执行绘制。 以TextView为例,具体流程如下:
FrameInfo分析
FrameInfo用来统计各个渲染阶段的耗时;可以通过FrameInfo的打印来分析耗时出现在哪个阶段。 当一帧绘制时间超过700ms时,log中会出现:Davey! duration=xxx
FrameInfo类图梳理