前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >hwui介绍与分析

hwui介绍与分析

作者头像
用户1907613
发布2023-09-17 14:58:16
1.9K0
发布2023-09-17 14:58:16
举报
文章被收录于专栏:Android群英传

hwui全称**HardwareAcceleratedRenderingEngineforUI,**hwui是一个基于GPU加速的2D图形引擎。HWUI的目标是提供高效、稳定、高质量的2D图形渲染能力,为Android系统的UI体验提供技术支持。

相关源码位于目录android/platform/framework/base/libs/hwui 文末有福利~

hwui的大部分代码以C++实现,Android平台对应到libhwui.so这个动态库.本文主要介绍一下hwui,简单梳理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类向外部提供的。

Canvas类图梳理

传给View系统的都是Canvas的派生类RecordingCanvas的实例;RecordingCanvas对应到Native层就是SkiaRecordingCanvas。SkiaRecordingCanvas里面有两个关键的成员变量:RecordingCanvas和SkiaDisplayList。RecordingCanvas是SkCanvas的派生类。

Canvas创建流程分析

Canvas一般都调用RenderNode的beginRecording函数创建。流程如下:

Canvas的draw函数分析

执行Canvas的draw call函数时,Canvas相关的绘制函数会调用到SkiaRecordingCanvas内的绘制函数;SkiaRecordingCanvas内的绘制函数会把绘制命令转化成Skia绘制命令,并调用RecordingCanvas内的绘制函数;RecordingCanvas内的绘制函数会调用DisplayListData内的绘制函数;DisplayListData内的绘制函数就会把绘制命令存起来。

代码语言:javascript
复制


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);
}

RenderNode分析

hwui一次渲染任务都是由一个个渲染节点RenderNode构成的,这些渲染节点组成树形结构。开始渲染时,从Root节点开始,以DFS方式进行遍历处理。

RenderNode类图梳理

RenderNode的渲染命令和数据存储在mDisplayList、mStagingDisplayList和mProperties、mStagingProperties。分析RenderNode就是分析mDisplayList、mStagingDisplayList和mProperties、mStagingProperties的设置逻辑、处理逻辑。

RenderNode创建流程分析

每个VIew持有一个RenderNode,同时HardwareRenderer持有一个RenderNode;HardwareRenderer持有的RenderNode为Root RenderNode,VIew持有的RenderNode为Sub RenderNode。HardwareRenderer的Root RenderNode在HardwareRenderer构造时被创建,VIew的RenderNode在VIew构造时被创建。

属性设置函数(如setTranslationX等)分析

RenderNode的属性设置函数(如setTranslationX等),会把属性值设置到mStagingProperties里面,并把mDirtyPropertiesFields对应的bit位设置成1。

beginRecording & endRecording函数分析

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)的子节点。

prepareTreeImpl函数分析

RenderNode的prepareTreeImpl函数会把mStagingProperties保存到mProperties、mStagingDisplayList保存到mDisplayList中,并递归调用mDisplayList的mChildNodes的prepareTreeImpl函数。

创建自己的RenderNode,并加入到View系统中

View & ViewGroup分析

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的设计后,再分析渲染流程就非常简单了。 渲染流程分为如下几个关键步骤:

  1. 执行RenderThread的requireGLContext函数:该函数主要是初始化egl环境和skia环境,创建EGLContext和GrContext。
  2. 创建EGLSurface。
  3. 执行最顶层View的updateDisplayListIfDirty函数,并把最顶层View的RenderNode加入到HardwareRenderer的RenderNode。
  4. 执行根RenderNode的prepareTreeImpl函数:会把RenderNode的mStagingProperties保存到mProperties,mStagingDisplayList保存到mDisplayList,并递归调用mDisplayList的mChildNodes的prepareTreeImpl函数。
  5. 执行SkiaOpenGLPipeline的draw函数:该函数首先创建一个和fb0绑定的SkSurface,然后把DisplayList里面的绘制命令取出来执行。 下面是具体的执行流程:
代码语言:javascript
复制


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类图梳理

FrameInfo数据填充流程

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-09-07 08:30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 群英传 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • hwui介绍
  • Canvas类图梳理
  • Canvas创建流程分析
  • RenderNode分析
    • RenderNode类图梳理
      • RenderNode创建流程分析
        • 属性设置函数(如setTranslationX等)分析
          • beginRecording & endRecording函数分析
            • prepareTreeImpl函数分析
            • View & ViewGroup分析
            • 触发绘制的流程分析
              • FrameInfo数据填充流程
              相关产品与服务
              数据保险箱
              数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档