硬件渲染一绘制阶段上层基本流程


启用硬件渲染

视图经过测量和布局,其大小和位置已经确定,接下来是绘制,将视图显示在固定的屏幕位置。本文基于硬件渲染进行分析,关于软件绘制知识不涉及。
每个App进程都包含各种不同的视图UI,系统并不知道你到底需要显示什么样的界面。因此,具体的绘制过程由App进程完成,系统层向你提供一块画布和一系列的api,我们就可以在自己的进程绘制各类视图View。
我们都知道,自定义视图时,需要重写View的onDraw方法,该方法的参数是Canvas,这块画布是如何产生的呢,硬件渲染中它到底扮演什么角色,利用画布api如何实现渲染呢,很多的疑问慢慢分析。
视图的测量,布局和渲染都在ViewRootImpl类中,performTraversals方法,在编舞者的控制下,当App收到刷新命令,实现一次绘制刷新。

private void performTraversals(){
    if (!cancelDraw && !newSurface) {//前面已经过测量与布局,且存在Surface。
        performDraw();
    } else {         
    }
}

//ViewRootImpl#performDraw方法。
private void performDraw() {
    final boolean fullRedrawNeeded = mFullRedrawNeeded;
    try {
        draw(fullRedrawNeeded);
    } finally {
    }
}

什么情况下开启硬件加速渲染?什么时候初始化HardwareRenderer呢?

在ViewRootImpl的setView方法中。

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    ...
    //view是DecorView视图,它实现RootViewSurfaceTaker
    if (view instanceof RootViewSurfaceTaker) {
        mSurfaceHolderCallback =((RootViewSurfaceTaker)view).willYouTakeTheSurface();
        if (mSurfaceHolderCallback != null) {
            mSurfaceHolder = new TakenSurfaceHolder();
            mSurfaceHolder.setFormat(PixelFormat.UNKNOWN);
        }
    }
    if (mSurfaceHolder == null) {
        enableHardwareAcceleration(attrs);//开启硬件加速
    }
    ...
}

private void enableHardwareAcceleration(WindowManager.LayoutParams attrs) {
    //应用在兼容模式时,不开启 
    //永久进程包括系统进程在低端设备中不加速
    ...
    //创建ThreadedRenderer对象
    mAttachInfo.mHardwareRenderer = HardwareRenderer.create(mContext, translucent);
    ...
}

mSurfaceHolderCallback不空时,mSurfaceHolder不空,Activity将拥有Surface,利用SurfaceHolder实现绘制,类似SurfaceView。
mSurfaceHolderCallback是空时,mSurfaceHolder也是空,启动硬件绘制。一个基本的视图都会使用硬件渲染。
开启硬件渲染,创建一个ThreadedRenderer,它继承HardwareRenderer。
回到ViewRootImpl的draw方法,AttachInfo内部HardwareRenderer存在,硬件渲染,不存在会软件渲染。

if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) {  
    mAttachInfo.mHardwareRenderer.draw(mView, mAttachInfo, this);//硬件渲染入口。
}

ThreadedRenderer架构分析

构造方法

ThreadedRenderer(Context context, boolean translucent) {
    ...
    long rootNodePtr = nCreateRootRenderNode();
    mRootNode = RenderNode.adopt(rootNodePtr);
    mRootNode.setClipToBounds(false);
    mNativeProxy = nCreateProxy(translucent, rootNodePtr);
    ...
}

JNI#方法创建一个底层RootRenderNode节点,创建一个上层RenderNode节点,封装RootRenderNode,即rootNodePtr指针。
JNI#方法创建一个底层RenderProxy代理。RenderProxy的构造方法,将创建CanvasContext和RenderThread,CanvasContext封装底层RootRenderNode节点,
ThreadedRenderer与底层结构的关系图如下。

ThreadedRenderer与底层结构的关系图.jpg

ThreadedRenderer的draw方法,Java层硬件渲染的入口方法。

@Override
void draw(View view, AttachInfo attachInfo, HardwareDrawCallbacks callbacks) {
    ...
    //入参view是顶层视图DecorView。
    updateRootDisplayList(view, callbacks);//遍历更新视图树每一个节点。
    final long[] frameInfo = choreographer.mFrameInfo.mFrameInfo;
    //同步绘制帧。
    int syncResult = nSyncAndDrawFrame(mNativeProxy, frameInfo, frameInfo.length);
    ...
}

updateRootDisplayList方法,绘制整个树形视图结构,从顶层视图开始,每一个视图节点逐一绘制,最终目的是触发每一个视图的Canvas#drawXxx方法。
nSyncAndDrawFrame方法,同步帧数据,最终目的OpenGL指令写入gpu。(另外文章分析)


视图遍历绘制

如何触发树形结构中每一个视图Canvas的相关方法呢?自定义View时,重写onDraw方法,将视图绘制成我们想要的样子,就是利用Canvas的一系列drawXxx方法。视图遍历,从ThreadedRenderer的updateRootDisplayList方法开始分析。

private void updateRootDisplayList(View view, HardwareDrawCallbacks callbacks) {
    //第一步,从顶层视图开始,更新所有视图的DisplayList。
    updateViewTreeDisplayList(view);
    //第二步,根节点绘制顶层视图RenderNode。
    if (mRootNodeNeedsUpdate || !mRootNode.isValid()) {
        DisplayListCanvas canvas = mRootNode.start(mSurfaceWidth, mSurfaceHeight);
        try {
            final int saveCount = canvas.save();
            canvas.translate(mInsetLeft, mInsetTop);
            callbacks.onHardwarePreDraw(canvas);
            //插入栅栏,隔离canvas操作
            canvas.insertReorderBarrier();
            //绘制顶层视图RenderNode。
            canvas.drawRenderNode(view.updateDisplayListIfDirty());
            //插入栅栏,隔离canvas操作
            canvas.insertInorderBarrier();
            //回调,ViewRootImpl中实现
            callbacks.onHardwarePostDraw(canvas);
            canvas.restoreToCount(saveCount);
            mRootNodeNeedsUpdate = false;
        } finally {
            mRootNode.end(canvas);
        }
    }
}

它的入参是顶层视图DecorView。主流程图。

绘制树形视图流程图.jpg
从图片与源码可以看出,两个步骤

  • updateViewTreeDisplayList方法,从顶层视图DecorView开始,遍历树形视图结构的每一个节点,利用视图内的RenderNode创建Canvas,绘制。
  • 利用ThreadedRenderer的根RootRenderNode创建Canvas,绘制顶层RenderNode节点。

根视图结构遍历绘制简要分析

遍历的主流程在第一步,ThreadedRenderer的updateViewTreeDisplayList方法,意思是更新树视图的绘制数据。从顶层视图DecorView,真正开始树形结构递归绘制。

private void updateViewTreeDisplayList(View view) {
    view.mPrivateFlags |= View.PFLAG_DRAWN;
    view.mRecreateDisplayList = (view.mPrivateFlags & 
                View.PFLAG_INVALIDATED)== View.PFLAG_INVALIDATED;
    view.mPrivateFlags &= ~View.PFLAG_INVALIDATED;
    view.updateDisplayListIfDirty();//即DecorView#updateDisplayListIfDirty方法。
    view.mRecreateDisplayList = false;//还原标志。
}

该方法代码很少,判断视图的PFLAG_INVALIDATED标志,有这个标志,在调用每一个View的updateDisplayListIfDirty方法时,才会创建Canvas,当一个视图需要绘制时,上层肯定会设置该标志。最后会将重建标志还原。流程图。
遍历递归绘制树形视图的处理流程.jpg

每个视图的流程是一样的,都有三个步骤,第一次绘制时,每个视图都要建立Canvas。

  • 通过视图RenderNode节点start方法,创建DisplayListCanvas画布对象。
  • 通过View的draw(canvas)方法,实现具体记录绘制操作,(绘制自身与派发),draw方法包括很多步骤,包括递归到子视图的updateDisplayListIfDirty方法,后面会介绍。
  • 最后,RenderNode结束记录end方法。

因此,从顶层DecorView的updateDisplayListIfDirty方法,就已经开始准备遍历递归每个子视图的updateDisplayListIfDirty方法啦,进入每一个视图的draw方法,最终,触发每个视图onDraw方法。

下面看一下View的updateDisplayListIfDirty方法源码。

public RenderNode updateDisplayListIfDirty() {
    final RenderNode renderNode = mRenderNode;
    // ThreadedRenderer是空,直接返回节点
    if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0
            || !renderNode.isValid()//false,还未记录绘制
            || (mRecreateDisplayList)) {//重建Canvas
        if (renderNode.isValid()
                && !mRecreateDisplayList) {
            mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            dispatchGetDisplayList();
            return renderNode; 
        }
        mRecreateDisplayList = true;//重建Canvas
        int width = mRight - mLeft;
        int height = mBottom - mTop;
        int layerType = getLayerType();
        //第一步,创建DisplayListCanvas
        final DisplayListCanvas canvas = renderNode.start(width, height);
        //判断LayerType,以及获取HardwareLayer
        try {
            final HardwareLayer layer = getHardwareLayer();
            if (layer != null && layer.isValid()) {
                canvas.drawHardwareLayer(layer, 0, 0, mLayerPaint);
            } else 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;
                //该View跳过绘制,则直接派发给子视图
                if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                    dispatchDraw(canvas);
                } else {
                    draw(canvas);//绘制,包括绘制本身,修饰,以及派发,共六个步骤。
                }
            }
        } finally {
            //第三步,绘制结束,保存canvas记录内容
            renderNode.end(canvas);
            setDisplayListProperties(renderNode);
        }
    } else {
        mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
    }
    return renderNode;
}

第一步 RenderNode#start方法开始,创建DisplayListCanvas画布,它继承Canvas。每个视图都有一个画布,Canvas缓存池存储画布对象。

public DisplayListCanvas start(int width, int height) {
    DisplayListCanvas canvas = DisplayListCanvas.obtain(this);
    canvas.setViewport(width, height);
    canvas.onPreDraw(null);
    return canvas;
}

从缓存池获取,将start方法调用者RenderNode设置成Canvas内部RenderNode,表示此Canvas正被该视图使用,视图与RenderNode也是对应关系。
缓存池不足时,创建DisplayListConvas,构造方法中,利用JNI#方法创建一个底层DisplayListCanvas对象,上层Canvas#mNativeCanvasWrapper保存底层指针。
Canvas的setViewport方法,在Java层DisplayListConvas,保存宽/高,JNI#nSetViewport方法,设置底层DisplayListCanvas宽/高,在底层CanvasState保存。
Canvas的onPreDraw方法,绘制操作前准备,底层DisplayListCanvas的prepare方法,默认触发底层prepareDirty(0.0f, 0.0f, width(), height()),入参是刚设置的宽/高区域。
底层DisplayListCanvas的prepareDirty方法。

void DisplayListCanvas::prepareDirty(float left, float top,
        float right, float bottom) {
    mDisplayListData = new DisplayListData();
    mState.initializeSaveStack(0, 0, mState.getWidth(), 
                mState.getHeight(), Vector3());
    mDeferredBarrierType = kBarrier_InOrder;
    mState.setDirtyClip(false);
    mRestoreSaveCount = -1;
}

准备将要绘制的脏区域,创建底层DisplayListData对象,CanvasState的initializeSaveStack初始化,创建Snapshot,初始化mRestoreSaveCount值-1。mDeferredBarrierType值为kBarrier_InOrder,确保创建Chunk。

上层DisplayListCanvas和底层的架构关系图。
上层DisplayListCanvas和底层的架构关系图.jpg

第二步 View#draw(canvas)方法,实现视图绘制。参数就是上面创建的DisplayListConvas画布,视图有一些公用绘制,例如背景,滚定条,修饰等。

@CallSuper
public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;//增加drawn标志
    ...
    if (!verticalEdges && !horizontalEdges) {
        // Step 3, draw the content
        if (!dirtyOpaque) onDraw(canvas);
        // Step 4, draw the children
        dispatchDraw(canvas);
        ...
        return;
    }
    ...
}

六个步骤。

  • 1:绘制背景。
  • 2:必要时保存canvas' layers,绘制边缘fade。
  • 3:onDraw方法,绘制视图内容,调用Canvas_api,自己实现。
  • 4:dispatchDraw派发绘制子视图,如果有跳过标志,将不会来到draw方法,直接去dispatchDraw。
  • 5:如有绘制fading edges,恢复canvas' layers。
  • 6:绘制修饰,如滚动条。

下面分析第三和四步骤。
第三步,onDraw方法,传入DisplayListConvas,调用drawXxxx方法。在View中,它是一个空方法,子类会实现,不管子类是叶子节点视图还是容器视图,该方法的主要目标是绘制自己。一般情况,容器视图重写的onDraw方法仅绘制一些边框,分割线之类的东西,而叶子节点视图的onDraw方法将绘制成用户想要得到的视图。
第四步,dispatchDraw方法,派发绘制子视图,在View中是空方法,容器类视图会重写。ViewGroup的dispatchDraw方法。

@Override
protected void dispatchDraw(Canvas canvas) {
    for (int i = 0; i < childrenCount; i++) {
        if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                child.getAnimation() != null) {
            more |= drawChild(canvas, child, drawingTime);
        }
    }
}

遍历子视图,drawChild方法,将触发子视图三个参数的draw重载方法。

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}

DecorView是容器视图,在他的dispatchDraw方法中,派发他的子视图的draw方法(三个参数重载)。
在三个参数的draw重载方法中,有一个判断,若是硬件渲染,触发子视图的updateDisplayListIfDirty方法。

if (drawingWithRenderNode) {
    renderNode = updateDisplayListIfDirty();
    if (!renderNode.isValid()) {
        renderNode = null;
        drawingWithRenderNode = false;
    }
}

因此,每个子视图都会递归到View的updateDisplayListIfDirty方法,进入此方法后,又回到和上面流程图一样的逻辑中,直到DecorView所有子视图绘制完成,并且所有子视图Canvas结束,递归完成,最后,DecorView的Canvas结束。
进入递归的点是在上面draw(Canvas canvas)方法的第四步dispatchDraw方法。如果视图getHardwareLayer方法不空,比如TextureView视图,触发Canvas的drawHardwareLayer方法,TextureView不是容器类型,是View的子类,没有绘制派发。

第三步 RenderNode#end方法,DisplayListCanvas结束,保存数据。

public void end(DisplayListCanvas canvas) {
    canvas.onPostDraw();
    long renderNodeData = canvas.finishRecording();
    nSetDisplayListData(mNativeRenderNode, renderNodeData);
    canvas.recycle();
    mValid = true;
}

JNI#方法调用底层DisplayListCanvas的finishRecording方法,返回底层DisplayListCanvas对象中保存的DisplayListData指针。
JNI#方法将DisplayListData指针保存到底层RenderNode中。
Canvas释放,回收入缓存池。

DisplayListData* DisplayListCanvas::finishRecording() {
    ...
    DisplayListData* data = mDisplayListData;
    mDisplayListData = nullptr;
    mSkiaCanvasProxy.reset(nullptr);
    return data;
}

记录DisplayList数据结束,将RenderNode有效标志设置为true,如果没有完成,DisplayList将无法显示,绘制结束后,视图RenderNode节点标示isValid有效标志。

ThreadedRenderer根RenderNode绘制简要分析

前面介绍过,updateRootDisplayList方法分两步,先顶层视图结构遍历绘制,更新DisplayList数据,第二步是ThreadedRenderer的根RenderNode绘制,同样,通过根RenderNode创建DisplayListCanvas,通过它的drawRenderNode方法,负责绘制顶层视图DecorView的RenderNode节点。**
这里需要注意的一点是,这里根RenderNode创建的Canvas不属于任一个View视图,因为Canvas是View的RenderNode搞出来的,顶层RenderNode不依赖任何View,Canvas负责绘制DecorView的RenderNode。
看一下DisplayListCanvas的drawRenderNode方法,其中还会调用一次根View的updateDisplayListIfDirty方法,不会再进行一次View树绘制,这时的view还是DecorView,它的DisplayListCanvas已经end结束记录,并且,View的RenderNode节点mValid已有效,且mRecreateDisplayList标志已被恢复。


总结

1,在Java层,硬件渲染由ThreadedRenderer负责,每个窗体根视图ViewRootImpl下都有一个ThreadedRenderer,保存在AttachInfo,它的draw方法是硬件渲染绘制的入口。
2,从ViewRootImpl开始,一般视图会创建ThreadedRenderer,启用硬件渲染,关键点在遍历每一个视图,根据视图RenderNode创建画布,有效绘制记录存储在RenderNode关联的底层DisplayListData
3,绘制架构包含RenderNode节点,DisplayListCanvas画布,底层DisplayListData对象,CanvasState状态存储对象,做完这些初始化工作,就可以在Java层画布上执行绘制操作方法啦,树形视图结构每一节点都有一个DisplayListCanvas,利用Canvas#drawXxx方法分别记录一些绘制操作,drawXxx画点、圆、矩形等操作,将这些操作存储在一个DisplayList集合中,这是App的UI线程负责的任务。
4,onDraw方法自己重写,View和ViewGroup有什么自己需要的绘制在这里完成。View的dispatchDraw是空方法,不做任何操作。ViewGroup重写dispatchDraw方法,实现绘制派发到子视图。容器视图一般没有自己要绘制的东西,可能在updateDisplayListIfDirty方法就已经选择dispatchDraw了。
5,顶层视图绘制入口是draw(一个参数)方法,在draw(一个参数)中,包含六个步骤,第四步会派发每个子视图,子视图绘制入口是draw(三个参数),在draw(三个参数)中,会根据硬件渲染,进入每个子视图updateDisplayListIfDirty方法,实现递归绘制。
6,当走到RenderNode的end方法时,表示视图本身以及子视图已经全部绘制完毕,也就是说当DecorView的RenderNode#end方准备执行时,所有draw已经完成。
7,View构造方法创建每一个视图的RenderNode。每一个RenderNode都会创建DisplayListCanvas,使用时是一一对应关系。


任重而道远

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

推荐阅读更多精彩内容