hwui介绍与分析

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(早期版本支持)等。
36ecca54ff6f44b8da7acb35f61fba94.png
● 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的派生类。
e3121e4a502ccdabe7153c9f62be33ff.png

Canvas创建流程分析

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

Canvas的draw函数分析

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

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方式进行遍历处理。
7fff0ad1911b633de90dc72f58745d8f.png

RenderNode类图梳理

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

RenderNode创建流程分析

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

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

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

beginRecording & endRecording函数分析

RenderNode的beginRecording函数会创建一个RecordingCanvas,并保存到mCurrentRecordingCanvas,然后把这个RecordingCanvas传到上层;上层通过这个Canvas执行的绘制指令,都会被存到该Canvas的DisplayList中。RenderNode的endRecording函数会把mCurrentRecordingCanvas中的mDisplayList保存到mStagingDisplayList。
c4a0fdc8dfdaafaaa5cbb1f178c42ee9.png
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函数。
c0161aef838e3510c6e1eda1168a272b.png

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

View & ViewGroup分析

View & ViewGroup是Android View系统UI控件基类。通过重载View类的onDraw函数可以绘制出各种各样的控件。
View和RenderNode一样都是树形结构。树的分支节点是ViewGroup类型,叶子节点是View类型。每个View实例对应一个RenderNode实例mRenderNode;View的onDraw函数中的canvas参数,就是通过mRenderNode获取的。
f85ae08fd0e09f9077e0575dd358ab0b.png

View类图梳理

526ca85e88f017e4213975a696de95fd.png

updateDisplayListIfDirty函数分析

updateDisplayListIfDirty函数触发View的绘制(此处说的绘制,是更新DisplayList);执行updateDisplayListIfDirty函数后,View就会绘制自己以及子View。draw函数、onDraw函数都是在此时触发执行的。下图是最顶层View的绘制流程。
3a6421bf5c0b3fc374ed839cc9853dca.png

渲染流程分析

初步了解了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里面的绘制命令取出来执行。
    下面是具体的执行流程:
    ee1d93316d9c4163346c596783e85a96.png

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为例,具体流程如下:
96f41c80236f37357d3545c8326b803e.png

FrameInfo分析

FrameInfo用来统计各个渲染阶段的耗时;可以通过FrameInfo的打印来分析耗时出现在哪个阶段。
当一帧绘制时间超过700ms时,log中会出现:Davey! duration=xxx

FrameInfo类图梳理

4509317b19f3f18a1292b75389b040e9.png

FrameInfo数据填充流程

8f14c9ced890e21099b9546fff9f333d.png

内推招聘帖

[上海/北京] 小红书 - 社区客户端团队 - 基础体验技术方向 - iOS/Android

岗位及团队介绍

小红书社区客户端-基础体验技术团队负责小红书社区主站核心业务的研发工作,包括首页主框架、全场景搜索业务、图文笔记业务、视频消费等核心场景的业务探索、性能体验优化、用户体验与架构优化等工作,你可以充分参与到业务的讨论和落地,也可以发挥主观能动性为小红书的发展助力,我们希望你积极主动,热爱移动端产品的研发,愿意深入钻研,提倡提效,反对内卷,做正确、艰难而有价值的事。

岗位要求

Android 开发工程师

大学本科或以上学历,计算机相关专业,3年以上 Android 相关经验
对移动研发充满热情,有较强的学习能力,好奇心和积极向上的心态
熟悉 Java/Kotlin 语言,熟悉 Android 系统 API,RxJava,Dagger2,以及 App 打包,测试,开发流程
代码基本功扎实,对数据结构及算法有一定程度的理解,良好的面向对象化编程思想,熟练运用常见设计模式
抗压能力强,具备良好的沟通表达能力和团队合作精神
有大型业务架构设计经验者优先,有跨端、动态化经验者优先

iOS 开发工程师

大学本科或以上学历,计算机相关专业,3年以上 iOS 相关经验
对移动研发充满热情,有较强的学习能力,好奇心和积极向上的心态
熟悉 Objective-C/Swift,熟悉 Cocoa 设计模式,深入理解 MVC MVVM
代码基本功扎实,对于常见的第三方库的使用和原理有一定的理解。对数据结构及算法有一定程度的理解,良好的面向对象化编程思想,熟练运用常见设计模式
抗压能力强,具备良好的沟通表达能力和团队合作精神
有大型业务架构设计经验者优先,有跨端、动态化经验者优先

联系方式
邮箱:[dkong@xiaohongshu.com]
联系人:扶摇
微信:bridge_k(加微信备注下公司+岗位+名字+工作经验)秒级通过
优势:Leader直招,秒级反馈,全程跟进,经验分享

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值