Android硬件加速过程分析

绘制基础

安卓绘制框架

绘制框架.png

应用层
1.通过与WindowManagerService通信,获取一块surface。
2.通过View系统或OpenGL、Skia在这块surface上绘制。
3.与WindowManagerService通信,提交绘制产生的缓存。
服务层(WindowManagerService)
1.生成、释放surface
2.管理surface的层级
3.获取surface的数据,将surface的数据进行混合,生成新的一帧。
4.将新的一帧数据写入内核,使得显示器刷新数据
关键类:
1.Canvas:画布,里面有一个Bitmap,用于保存绘制过程中的缓存。此类提供了绘制的基本方法,包括作圆、线等。
2.Paint:画笔,提供绘制的基本参数,比如说颜色,画笔的粗细等。
3.Surface:可以获取画布并提交绘制结果。本质上是一个代理类。用于Application与WindowManager通信。
4.ViewRootImp:实现ViewParent,包含Surface和Choreographer。用于控制Activity的绘制。

  1. Choreographer绘制调度器。用于控制各个Surface的刷新步调,让所以Surface基本上同时开始刷新。一般16ms触发一次刷新。
  2. SurfaceFlinger:混合各个Surface的数据,形成新的一帧数据。

View的重绘流程

以最基本的View的invalidate为例
基本绘制流程


绘制新流程.png

流程图中有小小错误,能找到吗?
基本流程可以分成两块:
一、触发刷新
子view调用invalidate,然后层层递进,调到ViewrootImp的ScheduleTraversals,在这个过程中不断确定重绘的区域,更新各个父控件的绘制标志。完成各个层级父控制的绘制标志的刷新之后,利用ViewrootImp中的Choreographer控制刷新的时机,触发刷新。
二、刷新过程

  • 首先通过Surface的lockCanvas申请一块新的画布,获取一个Canvas。
    值得注意的是,WindowManagerService会为每一块surface分配两块内存,
    一块作为显示使用(读),一块作为绘制使用(写)。在完成绘制后,两块内存的作用互换。lockCanvas的作用,实际上时是给待绘制的内存上锁,并且获取待绘制内存的地址,把这块内存作为画布的缓存。在lockCanvas时,会将显示的内存的数据拷贝到待绘制的内存中,同时传入一个区域(dirty),作为重绘的区域。每次重绘实际只需要绘制需要修改的区域(dirty区域),其它区域保持不变,这样,就大大加快了绘制效率

    绘制内存切换.png

  • 获取画布之后,就需要层层调用View的draw方法,在这块画布上在做画。
    每一个View都有一块缓存mDrawCache。View的绘制过程,首先要通过绘制标志,判断View的内容是否发生变化,有发生变化,则调用ondraw方法,重新获取mDrawCache。最后将mDrawCache的重绘区域绘制到画布中。
    ViewGroup包含自己本身的绘制和子View的绘制。自己本身的绘制和View一样,子View的绘制需要层层调用子View的绘制方法。

    draw过程.png

  • 完成绘制后,调用unlockCanvasAndPost接口。将绘制内容提交给WindowManagerService。
    接口unlockCanvasAndPost的实质是将绘制的内存解锁,并且通知WindowManagerService将两块内存的作用互换,这样,SurfaceFlinger就会将最新绘制后得到的内存进行混合显示。

硬件加速

为什么使用硬件加速

CPU和GPU架构

CPU : Central Processing Unit , 中央处理器,是计算机设备核心器件,用于执行程序代码。
GPU : Graphic Processing Unit , 图形处理器,主要用于处理图形运算,通常所说“显卡”的核心部件就是GPU。
下面是CPU和GPU的结构对比图。其中:

  • 黄色的Control为控制器,用于协调控制整个CPU的运行,包括取出指令、控制其他模块的运行等;

  • 绿色的ALU(Arithmetic Logic Unit)是算术逻辑单元,用于进行数学、逻辑运算;
    橙色的Cache和DRAM分别为缓存和RAM,用于存储信息。


    cpu和gpu架构图.png
  • 从结构图可以看出,CPU的控制器较为复杂,而ALU数量较少。因此CPU擅长各种复杂的逻辑运算,但不擅长数学尤其是浮点运算。

  • 和CPU不同的是,GPU就是为实现大量数学运算设计的。从结构图中可以看到,GPU的控制器比较简单,但包含了大量ALU。GPU中的ALU使用了并行设计,且具有较多浮点运算单元。

硬件加速的主要原理,就是通过底层软件代码,将CPU不擅长的图形计算转换成GPU专用指令,由GPU完成。

硬件加速的开启

Android 系统的 UI 从 绘制 到 显示在屏幕上 是分两个步骤的
第一步:在Android 应用程序这一侧进行的。(将 UI 构建到一个图形缓冲区 Buffer 中,交给SurfaceFlinger )
第二步:在SurfaceFlinger进程这一侧进行的。(获取Buffer 并合成以及显示到屏幕中。)
其中,第二步在 SurfaceFlinger 的操作一直是以硬件加速方式完成的,所以我们说的硬件加速一般指的是在 应用程序 图形通过GPU加速渲染 到 Buffer 的过程。

在Android中,可以四给不同层次上开启硬件加速:

  • 应用:
    <application android:hardwareAccelerated="true">
  • Activity
    <activity android:hardwareAccelerated="true">
  • Window
    getWindow().setFlags(WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED, WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED);
  • View
    view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
    在这四个层次中,应用和Activity是可以选择的,Window只能打开,View只能关闭。

硬件加速的流程分析

模式模型

构建阶段

所谓构建就是递归遍历所有视图,将需要的操作缓存下来,之后再交给单独的Render线程利用OpenGL渲染。在Android硬件加速框架中,View视图被抽象成RenderNode节点。


构建Rendor缓存.png

View中的绘制都会被抽象成一个个DrawOp(DisplayListOp),比如View中drawLine,构建中就会被抽象成一个DrawLintOp,drawBitmap操作会被抽象成DrawBitmapOp,每个子View的绘制被抽象成DrawRenderNodeOp,每个DrawOp有对应的OpenGL绘制命令,同时内部也握着绘图所需要的数据。

DrawOp.png

如此以来,每个View不仅仅握有自己DrawOp List,同时还拿着子View的绘制入口,如此递归,便能够统计到所有的绘制Op,很多分析都称为Display List,源码中也是这么来命名类的,不过这里其实更像是一个树,而不仅仅是List,示意如下:


displayList结构图-2.png

构建完成后,就可以将这个绘图Op树交给Render线程进行绘制,这里是同软件绘制很不同的地方,软件绘制时,View一般都在主线程中完成绘制,而硬件加速,除非特殊要求,一般都是在单独线程中完成绘制,如此以来就分担了主线程很多压力,提高了UI线程的响应速度。


CPU和GPU分工合作图.jpg

构建过程源码分析

构建阶段流程图.png

从流程图可以看出,HardwareRenderer是整个硬件加速绘制的入口。整个绘制的过程通过不断dispatchGetDisplayList或者dispathDraw遍历View,刷新displayList。
从名字能看出,ThreadedRenderer应该跟一个Render线程息息相关,不过ThreadedRenderer是在UI线程中创建的,那么与UI线程也必定相关,其主要作用:

1、在UI线程中完成DrawOp集构建
2、负责跟渲染线程通信

可见ThreadRenderer的作用是很重要的,简单看一下实现:

ThreadedRenderer(Context context, boolean translucent) {
    ...
    <!--新建native node-->
    long rootNodePtr = nCreateRootRenderNode();
    mRootNode = RenderNode.adopt(rootNodePtr);
    mRootNode.setClipToBounds(false);
    <!--新建NativeProxy-->
    mNativeProxy = nCreateProxy(translucent, rootNodePtr);
    ProcessInitializer.sInstance.init(context, mNativeProxy);
    loadSystemProperties();
}
  • RootNode用来标识整个DrawOp树的根节点,有个这个根节点就可以访问所有的绘制Op.
  • RenderProxy对象,这个对象就是用来跟渲染线程进行通信的句柄
    再看一下RenderProxy的源码
RenderProxy::RenderProxy(bool translucent, RenderNode* rootRenderNode, IContextFactory* contextFactory)
        : mRenderThread(RenderThread::getInstance())
        , mContext(nullptr) {
    SETUP_TASK(createContext);
    args->translucent = translucent;
    args->rootRenderNode = rootRenderNode;
    args->thread = &mRenderThread;
    args->contextFactory = contextFactory;
    mContext = (CanvasContext*) postAndWait(task);
    mDrawFrameTask.setContext(&mRenderThread, mContext);  
   }
  • RenderThread是一个单例线程,也就是说,每个进程最多只有一个硬件渲染线程,这样就不会存在多线程并发访问冲突问题,到这里其实环境硬件渲染环境已经搭建好好了。
    ThreadRender的绘制入口
@Override
void draw(View view, AttachInfo attachInfo, HardwareDrawCallbacks callbacks) {
    attachInfo.mIgnoreDirtyState = true;

    final Choreographer choreographer = attachInfo.mViewRootImpl.mChoreographer;
    choreographer.mFrameInfo.markDrawStart();
    <!--关键点1:构建View的DrawOp树-->
    updateRootDisplayList(view, callbacks);

    <!--关键点2:通知RenderThread线程绘制-->
    int syncResult = nSyncAndDrawFrame(mNativeProxy, frameInfo, frameInfo.length);
    ...
}
  • updateRootDisplayList,构建RootDisplayList,其实就是构建View的DrawOp树,updateRootDisplayList会进而调用根View的updateDisplayListIfDirty,让其递归子View的updateDisplayListIfDirty,从而完成DrawOp树的创建
  • 通知RenderThread线程绘制
    构建树的流程
private void updateRootDisplayList(View view, HardwareDrawCallbacks callbacks) {
    <!--更新-->
    updateViewTreeDisplayList(view);
   if (mRootNodeNeedsUpdate || !mRootNode.isValid()) {
      <!--获取DisplayListCanvas-->
        DisplayListCanvas canvas = mRootNode.start(mSurfaceWidth, mSurfaceHeight);
        try {
        <!--利用canvas缓存Op-->
            final int saveCount = canvas.save();
            canvas.translate(mInsetLeft, mInsetTop);
            callbacks.onHardwarePreDraw(canvas);

            canvas.insertReorderBarrier();
            canvas.drawRenderNode(view.updateDisplayListIfDirty());
            canvas.insertInorderBarrier();

            callbacks.onHardwarePostDraw(canvas);
            canvas.restoreToCount(saveCount);
            mRootNodeNeedsUpdate = false;
        } finally {
        <!--将所有Op填充到RootRenderNode-->
            mRootNode.end(canvas);
        }
    }
}
  • 利用View的RenderNode获取一个DisplayListCanvas
  • 利用DisplayListCanvas构建并缓存所有的DrawOp
  • 将DisplayListCanvas缓存的DrawOp填充到RenderNode
  • 将根View的缓存DrawOp设置到RootRenderNode中,完成构建
    DisplayList的更新过程
  // Don't need to recreate the display list, just need to tell our
            // children to restore/recreate theirs
            if (renderNode.isValid()
                    && !mRecreateDisplayList) {
                mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                dispatchGetDisplayList();

                return renderNode; // no work needed
            }

            // If we got here, we're recreating it. Mark it as such to ensure that
            // we copy in child display lists into ours in drawChild()
            mRecreateDisplayList = true;

            int width = mRight - mLeft;
            int height = mBottom - mTop;
            int layerType = getLayerType();

            final DisplayListCanvas canvas = renderNode.start(width, height);

            try {
                if (layerType == LAYER_TYPE_SOFTWARE) {
                    buildDrawingCache(true);
                    Bitmap cache = getDrawingCache(true);
                    if (cache != null) {
                        canvas.drawBitmap(cache, 0, 0, mLayerPaint);
                    }
                } else {
                    computeScroll();

                    canvas.translate(-mScrollX, -mScrollY);
                    mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
                    mPrivateFlags &= ~PFLAG_DIRTY_MASK;

                    // Fast path for layouts with no backgrounds
                    if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                        dispatchDraw(canvas);
                        drawAutofilledHighlight(canvas);
                        if (mOverlay != null && !mOverlay.isEmpty()) {
                            mOverlay.getOverlayView().draw(canvas);
                        }
                        if (debugDraw()) {
                            debugDrawFocus(canvas);
                        }
                    } else {
                        draw(canvas);
                    }
                }
            } finally {
                renderNode.end(canvas);
                setDisplayListProperties(renderNode);
            }
  • 如果本View不需要更新RendorNode,则调用dispathcGetDisplayList,让其子view更新RendorNode。最终会导致步骤2。
  • 如果本View需要更新RendorNode,则调用draw(Canvas)方法。
    继续跟踪draw(Canvas)方法
 if (!dirtyOpaque) {
            drawBackground(canvas);
        }

        // skip step 2 & 5 if possible (common case)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);

            drawAutofilledHighlight(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);

我们知道每个view的DrawRenderNodeOp缓存主要包括displayListOps和其子View的DrawRenderNodeOp

  • drawBackground、onDraw 、onDrawForeground、drawDefaultFocusHighlight是更新自身相关的displayListOps
  • dispatchDraw向下触发便利,更新子View相关的DrawRenderNodeOp。


    drawOps更新.png

    dispatchDraw最终会走到draw(Canvas canvas, ViewGroup parent, long drawingTime) ,看一下里面的代码

....
   if (drawingWithRenderNode) {
            // Delay getting the display list until animation-driven alpha values are
            // set up and possibly passed on to the view
            renderNode = updateDisplayListIfDirty();
            if (!renderNode.isValid()) {
                // Uncommon, but possible. If a view is removed from the hierarchy during the call
                // to getDisplayList(), the display list will be marked invalid and we should not
                // try to use it again.
                renderNode = null;
                drawingWithRenderNode = false;
            }
        }
 if (!drawingWithDrawingCache) {
            if (drawingWithRenderNode) {
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                ((DisplayListCanvas) canvas).drawRenderNode(renderNode);
            } else {
                // Fast path for layouts with no backgrounds
                if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                    mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                    dispatchDraw(canvas);
                } else {
                    draw(canvas);
                }
            }
        } 
....
  • 子view通过updateDisplayListIfDirty刷新自己的RendNode。如果子View本身,以及子View的子控件没有变化,RenderNode不会发生实质的变化。
  • 最后通过DisplayListCanvas的drawRenderNode将绘制缓存保存到父View 中

DisplayListCanvas类分析,以drawLine为例

void DisplayListCanvas::drawLines(const float* points, int count, const SkPaint& paint) {
    points = refBuffer<float>(points, count);

    addDrawOp(new (alloc()) DrawLinesOp(points, count, refPaint(&paint)));
}

RenderThread渲染过程

DrawOp树构建完毕后,UI线程利用RenderProxy向RenderThread线程发送一个DrawFrameTask任务请求,RenderThread被唤醒,开始渲染。

  • 首先进行DrawOp的合并
  • 接着绘制特殊的Layer
  • 最后绘制其余所有的DrawOpList
  • 调用swapBuffers将前面已经绘制好的图形缓冲区提交给Surface Flinger合成和显示。

开启硬件加速的优缺点

优点

  • 硬件加速使用双线程工作,主线程只负责构建绘制树,渲染线程负责渲染。主线程基本上不会因为绘制超时而卡顿。
  • GPU分担了CPU的渲染任务,CPU有多余的时间做其他重要的事情,这将使得手机整体表现将更加流畅。
  • GPU擅长做渲染工作,硬件加速允许应用执行繁重的渲染任务。如果没有GPU或者不采用硬件加速,播放视频将非常卡顿
    缺点
  • 硬件加速属于双缓冲机制,使用显存进行页面渲染(使用较少的物理内存),导致更频繁的显存操作,可能引起以下现象:
    花屏、闪屏
  • 硬件加速的准备工作较长,可能在应用刚启动时,存在掉帧现象。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容