Android 自定义View之Draw过程(下)

329

前言

Draw 过程涉及知识较多,分为上中下三篇

上篇分析了硬件加速相关知识:
Android 自定义View之Draw过程(中)
本篇将从代码的角度深入分析硬件加速绘制与软件绘制。
通过本篇文章,你将了解到:

1、软件绘制流程
2、硬件加速绘制流程
3、LayerType 对绘制的影响
4、Canvas 从哪里来到哪里去
5、绘制流程全家福

软件绘制流程

上篇说过在ViewRootImpl->draw(xx)里软件绘制与硬件加速绘制分道扬镳:

image.png

上图是Window 区分硬件加速绘制与软件绘制的入口。
由易到难,先来看看软件绘制流程。

drawSoftware(xx)

#ViewRootImpl.java
    private boolean drawSoftware(Surface surface, AttachInfo attachInfo, int xoff, int yoff,
                                 boolean scalingRequired, Rect dirty, Rect surfaceInsets) {
        //持有的画布
        final Canvas canvas;
        ...
        try {
            ...
            //申请画布对象,该画布初始大小为dirty的尺寸
            canvas = mSurface.lockCanvas(dirty);
            //设置密度
            canvas.setDensity(mDensity);
        } catch (Surface.OutOfResourcesException e) {
            ...
        } catch (IllegalArgumentException e) {
            ...
            return false;
        } finally {
            ...
        }

        try {
            //画布是否需要移动
            canvas.translate(-xoff, -yoff);
            //mView 即是添加到该Window的RootView
            //对于Activity、Dialog开启的Window,mView就是我们熟知的DecorView
            //rootView draw()方法
            mView.draw(canvas);
        } finally {
            try {
                //提交绘制的内容到Surface
                surface.unlockCanvasAndPost(canvas);
            } catch (IllegalArgumentException e) {
                ...
            }
        }
        return true;
    }

以上方法功能重点如下:

1、从Surface 申请Canvas对象,该Canvas为CompatibleCanvas 类型
2、拿到Canvas后,调用View.draw(Canvas)开始绘制RootView
3、整个ViewTree 绘制完成后将内容提交到Surface

注:RootView 只是个代称,并不是某个View的名字。
一些常见的RootView 请移步:Android 输入事件一撸到底之源头活水(1)

关于View.draw(xx)方法在:Android 自定义View之Draw过程(上) 已做过详细分析,结合上述代码,用如下图表示:

image.png

可以看得出来,软件绘制有如下特点:

  • 从RootView 递归调用子布局的draw(xx)方法,直到每个符合条件的View都进行了绘制
  • 绘制过程中,所有的View持有相同的Canvas对象

引入问题1:既然所有的View都持有相同的Canvas,那么每个View绘制的起点、终点是如何确定的呢?
该问题稍后分析。

硬件加速绘制流程

概要

软件绘制是将Canvas的一系列操作写入到Bitmap里,而对于硬件加速绘制来说,每个View 都有一个RenderNode,当需要绘制的时候,从RenderNode里获取一个RecordingCanvas,与软件绘制一样,也是调用Canvas一系列的API,只不过调用的这些API记录为一系列的操作行为存放在DisplayList。当一个View录制结束,再将DisplayList交给RenderNode。此时,绘制的步骤已经记录在RenderNode里,到此,针对单个View的硬件绘制完成,这个过程也称作为DisplayList的构建过程。

调用过程分析

来看看硬件加速的入口:

#ThreadedRenderer.java
    void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) {
        ...
        //(1)--->录制操作
        //更新根View的DisplayList
        //从此处开始将绘制操作记录到DisplayList里
        //最终记录在rootRenderNode里
        updateRootDisplayList(view, callbacks);

        //(2)--->渲染
        //渲染绘制的内容
        //以proxy为桥梁,而proxy又与rootRenderNode关联
        //因此最终将上一步记录的绘制操作交给单独的线程渲染
        int syncResult = syncAndDrawFrame(choreographer.mFrameInfo);
        ...
    }

重点关注录制操作过程,接着来分析它:

#ThreadedRenderer.java
    private void updateRootDisplayList(View view, DrawCallbacks callbacks) {
        //遍历ViewTree,构建DisplayList
        updateViewTreeDisplayList(view);

        //当ViewTree DisplayList构建完毕后
        //一开始mRootNode 是没有DisplayList
        if (mRootNodeNeedsUpdate || !mRootNode.hasDisplayList()) {
            //申请Canvas
            RecordingCanvas canvas = mRootNode.beginRecording(mSurfaceWidth, mSurfaceHeight);
            try {
                ...
                //view.updateDisplayListIfDirty() 返回的是RootView 关联的renderNode
                //现在将RootView renderNode挂到canvas下,这样子就串联起所有的renderNode了
                canvas.drawRenderNode(view.updateDisplayListIfDirty());
                ...
                mRootNodeNeedsUpdate = false;
            } finally {
                //最后将DisplayList 挂到renderNode下
                mRootNode.endRecording();
            }
        }
    }

    private void updateViewTreeDisplayList(View view) {
        //标记该View已绘制过
        view.mPrivateFlags |= View.PFLAG_DRAWN;
        //mRecreateDisplayList --> 表示该View 是否需要重建DisplayList,也就是重新录制,更直白地说是否需要走Draw 过程
        //若是打上了 PFLAG_INVALIDATED 标记,也就是该View需要刷新,则需要重建
        view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED)
                == View.PFLAG_INVALIDATED;
        //清空原来的值
        view.mPrivateFlags &= ~View.PFLAG_INVALIDATED;
        //如果有需要,更新View的DisplayList
        view.updateDisplayListIfDirty();
        //View 已经重建完毕,无需再重建
        view.mRecreateDisplayList = false;
    }

以上调用了到了View里的方法:updateDisplayListIfDirty()。
顾名思义,如果有需要更新View的DisplayList。

#View.java
    public RenderNode updateDisplayListIfDirty() {
        //每个View构造的时候都会创建一个RenderNode:mRenderNode,称之为渲染节点
        final RenderNode renderNode = mRenderNode;
        //是否支持硬件加速,通过判断View.AttachInfo.mThreadedRenderer
        if (!canHaveDisplayList()) {
            return renderNode;
        }

        //取出该View的标记
        //1、绘制缓存失效 2、渲染节点还没有DisplayList 3、渲染节点有DisplayList,但是需要更新
        //三者满足其中一个条件,则进入条件代码块
        if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0
                || !renderNode.hasDisplayList()
                || (mRecreateDisplayList)) {
            //如果有DisplayList且该DisplayList无需更新,则说明该View不需要重新走Draw过程
            if (renderNode.hasDisplayList()
                    && !mRecreateDisplayList) {
                //标记该View已经绘制完成且缓存是有效的
                mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                //继续查看子布局是否需要构建DisplayList
                dispatchGetDisplayList(); //---------(1)

                return renderNode; // no work needed
            }
            
            //上述条件不满足,则说明该View需要构建DisplayList
            mRecreateDisplayList = true;
            //layout 过程确定的View的坐标此时用到了
            int width = mRight - mLeft;
            int height = mBottom - mTop;
            //获取当前设置的layerType
            int layerType = getLayerType();
            //从renderNode里获取Canvas对象,Canvas的尺寸初始化为View的尺寸
            //该Canvas是RecordingCanvas类型,简单理解为用来录制的Canvas
            final RecordingCanvas canvas = renderNode.beginRecording(width, height);

            try {
                //layerType 有三种取值
                //如果是软件绘制缓存
                if (layerType == LAYER_TYPE_SOFTWARE) {
                    //---------(2)
                    //则构建缓存
                    buildDrawingCache(true);
                    //实际上就是将绘制操作写入Bitmap里
                    Bitmap cache = getDrawingCache(true);
                    if (cache != null) {
                        //将该Bitmap绘制到Canvas里
                        canvas.drawBitmap(cache, 0, 0, mLayerPaint);
                    }
                } else {
                    //如果没有设置软件绘制缓存
                    
                    //一般配合Scroller 滑动使用
                    computeScroll();
                    //mScrollX、mScrollY 为滚动的距离
                    //当mScrollX 为正值时,canvas向左移动,绘制的内容往左移动,这也就是为什么明明scroll为正值,为啥View内容往左移的根本原因
                    canvas.translate(-mScrollX, -mScrollY);
                    mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
                    mPrivateFlags &= ~PFLAG_DIRTY_MASK;

                    //---------(3)
                    if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                        //该View不需要绘制自身内容(包括内容、前景、背景等)
                        //直接发起绘制子布局的请求
                        dispatchDraw(canvas);
                        drawAutofilledHighlight(canvas);
                        if (mOverlay != null && !mOverlay.isEmpty()) {
                            mOverlay.getOverlayView().draw(canvas);
                        }
                        if (debugDraw()) {
                            debugDrawFocus(canvas);
                        }
                    } else {
                        //需要绘制自身
                        draw(canvas);
                    }
                }
            } finally {
                //最后结束canvas录制,并将录制产生的结果:DisplayList交给renderNode
                //---------(4)
                renderNode.endRecording();
                setDisplayListProperties(renderNode);
            }
        } else {
            //三个条件不满足,认为已经绘制完成
            mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
        }
        //返回renderNode 到上一层
        //---------(5)
        return renderNode;
    }

注释里列出了5个比较重要的点,来一一解析:
(1)
dispatchGetDisplayList()
该方法在View里没有实现,在ViewGroup实现如下:

#ViewGroup.java
    protected void dispatchGetDisplayList() {
        final int count = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < count; i++) {
            //遍历子布局
            final View child = children[i];
            if (((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null)) {
                //重建子布局DisplayList
                recreateChildDisplayList(child);
            }
        }
        ...
    }

    private void recreateChildDisplayList(View child) {
        //判断是否需要重建
        child.mRecreateDisplayList = (child.mPrivateFlags & PFLAG_INVALIDATED) != 0;
        child.mPrivateFlags &= ~PFLAG_INVALIDATED;
        //调用子布局重建方法
        child.updateDisplayListIfDirty();
        child.mRecreateDisplayList = false;
    }

可以看出,dispatchGetDisplayList 作用:

遍历子布局,并调用它们的重建方法

这样子,从RootView开始递归调用updateDisplayListIfDirty(),如果子布局需要重建DisplayList,则重新录制绘制操作,否则继续查找子布局是否需要重建DisplayList。

(2)
buildDrawingCache(xx) 用来绘制离屏缓存,后续再细说。

(3)
跳过绘制这段可参考:Android ViewGroup onDraw为什么没调用

(4)
硬件加速绘制有开始、录制、结束的标记:

1、renderNode生成用来绘制的Canvas--> beginRecording,此为开始。
2、调用Canvas.drawXX()--> 录制具体的东西,此为录制过程
3、renderNode结束绘制--> endRecording(),从Canvas里拿到录制的结果:DisplayList,并将该结果赋值给renderNode,此为录制结束

(5)
从第4点可以看出,录制的结果已经存放到RenderNode里,需要将RenderNode返回,该RenderNode将会被挂到父布局的Canvas里,也就是说父布局Canvas已经持有了子布局录制好的DisplayList。

简单一些,用图表示单个View的硬件加速绘制流程:

image.png

ViewTree 硬件加速过程:

image.png

很明显,硬件加速绘制过程就是构建DisplayList过程,从RootView递归子布局构建DisplayList,当整个DisplayList构建完毕,就可以进行渲染了,渲染线程交给GPU处理,这样子大大解放了CPU工作。

LayerType 对绘制的影响

以上分别阐述了软件绘制与硬件加速绘制的流程,分析的起点是该Window是否支持硬件加速而走不同的分支。
从RootView开始到遍历所有的子孙View,要么都是软件绘制,要么都是硬件加速绘制,如果在硬件加速绘制的中途禁用了某个View的硬件加速会如何表现呢?我们之前提到过通过设置View->LayerType来禁用硬件加速,接下来分析LayerType对绘制流程的影响。
Android 自定义View之Draw过程(上) 分析可知:不管软件绘制或者硬件加速绘制,都会走一套公共的流程:

draw(xx)->dispatchDraw(xx)->draw(x1,x2,x3)->draw(xx)...

image.png

这也是递归调用的过程。
对于单个View,软件绘制与硬件加速分歧点在哪呢?
答案是:draw(x1,x2,x3)方法

View的软硬绘制分歧点

#View.java
    boolean draw(Canvas canvas, ViewGroup parent, long drawingTime) {
        //canvas是否支持硬件加速
        //默认canvas是不支持硬件加速的
        //RecordingCanvas支持硬件加速
        final boolean hardwareAcceleratedCanvas = canvas.isHardwareAccelerated();

        //是否使用RenderNode绘制,也就是该View是否支持硬件加速
        //该View支持硬件加速的条件是:canvas支持硬件加速+该Window支持硬件加速
        boolean drawingWithRenderNode = mAttachInfo != null
                && mAttachInfo.mHardwareAccelerated
                && hardwareAcceleratedCanvas;
        //动画相关
        ...

        if (hardwareAcceleratedCanvas) {
            //canvas支持硬件加速,需要检测是否需要重建DisplayList
            mRecreateDisplayList = (mPrivateFlags & PFLAG_INVALIDATED) != 0;
            mPrivateFlags &= ~PFLAG_INVALIDATED;
        }

        RenderNode renderNode = null;
        Bitmap cache = null;
        //获取LayerType,View 默认类型是None
        int layerType = getLayerType();
        //------>(1)
        if (layerType == LAYER_TYPE_SOFTWARE || !drawingWithRenderNode) {
            //1、设置了离屏软件绘制缓存 2、View不支持硬件加速绘制
            //两者满足其一
            if (layerType != LAYER_TYPE_NONE) {
                //可能设置了软件缓存或者硬件缓存
                //此时硬件缓存当做软件缓存来使用
                layerType = LAYER_TYPE_SOFTWARE;
                //绘制到软件缓存
                //------>(2)
                buildDrawingCache(true);
            }
            //取出软件缓存
            cache = getDrawingCache(true);
        }

        if (drawingWithRenderNode) { //----->(3)
            //该View支持硬件加速
            //则尝试构建DisplayList,并返回renderNode
            renderNode = updateDisplayListIfDirty();
            if (!renderNode.hasDisplayList()) {
                //一般很少走这
                renderNode = null;
                drawingWithRenderNode = false;
            }
        }

        int sx = 0;
        int sy = 0;
        if (!drawingWithRenderNode) {
            computeScroll();
            //不使用硬件加速时将内容偏移记录
            sx = mScrollX;
            sy = mScrollY;
        }

        //注意这两个标记,下面会用到
        //1、存在软件缓存 2、不支持硬件加速 两者同时成立,则说明:使用软件缓存绘制
        final boolean drawingWithDrawingCache = cache != null && !drawingWithRenderNode;
        //1、不存在软件缓存 2、不支持硬件加速,两者同时成立,则说明:使用软件绘制
        final boolean offsetForScroll = cache == null && !drawingWithRenderNode;

        if (offsetForScroll) {
            //------>(4)
            //如果是软件绘制,需要根据View的偏移与内容偏移移动canvas
            //此时包括内容滚动偏移量
            canvas.translate(mLeft - sx, mTop - sy);
        } else {
            if (!drawingWithRenderNode) {
                //------>(5)
                //如果不支持硬件加速,则说明可能是软件缓存绘制
                //此时也需要位移canvas,只不过不需要考虑内容滚动偏移量
                canvas.translate(mLeft, mTop);
            }
            ...
        }

        ...

        if (!drawingWithRenderNode) {
            //不支持硬件加速
            if ((parentFlags & ViewGroup.FLAG_CLIP_CHILDREN) != 0 && cache == null) {
                //裁减canvas,限制canvas展示区域,这就是子布局展示为什么不能超过父布局区域的原因
                if (offsetForScroll) {
                    //是软件绘制,则裁减掉滚动的距离
                    //------>(6)
                    canvas.clipRect(sx, sy, sx + getWidth(), sy + getHeight());
                } else {
                    //否则无需考虑滚动距离
                    if (!scalingRequired || cache == null) {
                        canvas.clipRect(0, 0, getWidth(), getHeight());
                    } else {
                        canvas.clipRect(0, 0, cache.getWidth(), cache.getHeight());
                    }
                }
            }
            ...
        }

        if (!drawingWithDrawingCache) {
            //不使用软件缓存绘制
            if (drawingWithRenderNode) {
                //支持硬件加速
                mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                //将该View的renderNode挂到父布局的Canvas下,此处建立了连接
                ((RecordingCanvas) canvas).drawRenderNode(renderNode);
            } else {
                //软件绘制,发起了绘制请求:dispatchDraw(canvas) & draw(canvas);
                //------>(7)
                if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                    mPrivateFlags &= ~PFLAG_DIRTY_MASK;
                    dispatchDraw(canvas);
                } else {
                    draw(canvas);
                }
            }
        } else if (cache != null) {
            //软件绘制缓存存在
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            if (layerType == LAYER_TYPE_NONE || mLayerPaint == null) {
                ...
                //没有设置缓存类型,则将软件绘制缓存写入到canvas的bitmap里
                canvas.drawBitmap(cache, 0.0f, 0.0f, cachePaint);
            } else {
                ...
                canvas.drawBitmap(cache, 0.0f, 0.0f, mLayerPaint);
            }
        }
        ...
        //该View 构建完毕
        mRecreateDisplayList = false;
        return more;
    }

该方法里面的判断比较乱,提取了比较重要的7个点:
(1)
只要设置了离屏软件缓存或者不支持硬件加速,那么就需要使用软件缓存绘制。

(3)
只要支持硬件加速,则使用硬件加速绘制。结合(1),是不是觉得有点矛盾呢?想想满足(1)条件的情况之一:设置了离屏软件缓存,也支持硬件加速,按照(1)的逻辑,那么此时启用了软件缓存绘制。那么(3)继续用硬件加速绘制不是多此一举吗?
回顾一下updateDisplayListIfDirty()里的片段:

        if (layerType == LAYER_TYPE_SOFTWARE) {
            ...
            //软件缓存绘制
            buildDrawingCache(true);
        } else {
            //硬件绘制
            ...
        }

这里边再次进行了判断。

(4)(5)
Canvas位移
对于软件绘制,将Canvas进行位移,位移距离考虑了View本身偏移以及View内容偏移。
对于软件缓存绘制,将Canvas进行位移,仅仅考虑了View本身偏移。
对于硬件加速绘制,没看到对Canvas进行位移。
实际上针对软件缓存绘制与硬件加速绘制,Canvas位移既包括View本身偏移也包含了View内容偏移。只是不在上述的代码里。
对于软件缓存绘制:

在buildDrawingCacheImpl(xx) -> canvas.translate(-mScrollX, -mScrollY);进行了内容偏移。

而对于硬件加速绘制:

在layout(xx)->mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom) 进行了View本身的偏移。
在updateDisplayListIfDirty(xx)->canvas.translate(-mScrollX, -mScrollY);进行了内容偏移。

因此,不论软件绘制/软件缓存绘制/硬件加速绘制,三者都对Canvas进行了位移,位移包括:View本身的偏移以及内容的偏移。

以上也解释了问题1。

(6)
Canvas裁减
对于软件绘制,Canvas裁减包括了View内容偏移。
对于软件缓存绘制,Canvas 绘制到Bitmap里。
对于硬件加速绘制,在setDisplayListProperties(xx)->renderNode.setClipToBounds(xx) 进行裁减。

(7)
如果是软件绘制,那么直接调用dispatchDraw(xx)/draw(xx)发起绘制。

draw(x1,x2,x3)方法作用:决定View是使用何种绘制方式:

1、硬件加速绘制
2、软件绘制
3、软件缓存绘制

软件缓存绘制

来看看如何构建软件缓存:

#View.java
    public void buildDrawingCache(boolean autoScale) {
        //如果缓存标记为失效或者缓存为空
        if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0 || (autoScale ?
                mDrawingCache == null : mUnscaledDrawingCache == null)) {
            try {
                //构建缓存
                buildDrawingCacheImpl(autoScale);
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }
        }
    }

    private void buildDrawingCacheImpl(boolean autoScale) {
        int width = mRight - mLeft;
        int height = mBottom - mTop;
        ...

        boolean clear = true;
        Bitmap bitmap = autoScale ? mDrawingCache : mUnscaledDrawingCache;
        //bitmap 不存在或者bitmap与View尺寸不一致,则创建
        ...
        Canvas canvas;
        if (attachInfo != null) {
            canvas = attachInfo.mCanvas;
            if (canvas == null) {
                //第一次,AttachInfo里并没有Canvas
                canvas = new Canvas();
            }
            //关联bitmap
            canvas.setBitmap(bitmap);
            attachInfo.mCanvas = null;
        } else {
            //很少走这
            canvas = new Canvas(bitmap);
        }
        computeScroll();
        final int restoreCount = canvas.save();
        //根据内容滚动平移
        canvas.translate(-mScrollX, -mScrollY);

        mPrivateFlags |= PFLAG_DRAWN;
        if (mAttachInfo == null || !mAttachInfo.mHardwareAccelerated ||
                mLayerType != LAYER_TYPE_NONE) {
            //打上标记,说明软件绘制缓存已生效
            mPrivateFlags |= PFLAG_DRAWING_CACHE_VALID;
        }
        
        //同样的,调用公共方法
        if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            dispatchDraw(canvas);
            drawAutofilledHighlight(canvas);
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().draw(canvas);
            }
        } else {
            draw(canvas);
        }

        canvas.restoreToCount(restoreCount);
        canvas.setBitmap(null);

        if (attachInfo != null) {
            //记录下来,下次创建直接使用
            attachInfo.mCanvas = canvas;
        }
    }

如此一来,软件缓存就构建完成了,其结果存储在Bitmap里,可以通过如下方法获取:

#View.java
    public Bitmap getDrawingCache(boolean autoScale) {
        //禁止使用软件缓存
        //默认不禁止
        if ((mViewFlags & WILL_NOT_CACHE_DRAWING) == WILL_NOT_CACHE_DRAWING) {
            return null;
        }
        if ((mViewFlags & DRAWING_CACHE_ENABLED) == DRAWING_CACHE_ENABLED) {
            //是否开启了软件缓存绘制,默认不开启
            //构建缓存
            buildDrawingCache(autoScale);
        }
        //将缓存返回
        return autoScale ? mDrawingCache : mUnscaledDrawingCache;
    }

该方法可用来获取View的页面。
做一个小结:

一开始,硬件加速绘制流程和软件绘制流程各走各的互不影响。
1、使用软件绘制时候,设置了离屏缓存类型:软件缓存,则软件绘制失效,仅仅使用软件缓存绘制。设置了硬件缓存类型也当做软件缓存绘制。
2、使用硬件加速绘制的时候,设置了离屏缓存类型:软件缓存,则硬件加速绘制失效,仅仅使用软件缓存绘制。这也就是为什么设置软件缓存可以禁用硬件加速的原因。
3、软件缓存绘制的结果保存在bitmap里,该Bitmap最终会绘制到父布局的Canvas里。

不管使用哪种绘制类型,都会走共同的调用方法:draw(xx)/dispatchDraw(xx)。
因此,绘制类型对于我们重写onDraw(xx)是透明的。

Canvas 从哪里来到哪里去

软件绘制
从ViewRootImpl->drawSoftware(xx)开始,通过:

canvas = mSurface.lockCanvas(dirty);

生成了Canvas。该Canvas通过View.draw(xx)方法传递给所有的子布局,因此此种情形下,整个ViewTree共享同一个Canvas对象。Canvas类型为:CompatibleCanvas。
硬件加速绘制
从View->updateDisplayListIfDirty(xx)开始,通过:

final RecordingCanvas canvas = renderNode.beginRecording(width, height);

生成了Canvas。可以看出,对于每个支持硬件加速的View都重新生成了Canvas。Canvas类型为:RecordingCanvas。

软件缓存绘制
从View->buildDrawingCacheImpl(xx)开始,通过:

canvas = new Canvas();

生成了Canvas,并将该Canvas记录在AttachInfo里,下次再次构建该View软件缓存时拿出来使用。可以看出,对于每个使用了软件缓存的View都生成了新的Canvas,当然如果AttachInfo有,就可以重复使用。

脱离View的Canvas
以上三者有个共同的特点:所生成的Canvas最终都与Surface建立了联系,因此通过这些Canvas绘制的内容最终能够展示在屏幕上。
那是否可以直接构造脱离View的Canvas呢?答案是可以的。

    private void buildCanvas(int width, int height) {
        Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        Canvas canvas = new Canvas();
        canvas.setBitmap(bitmap);
        
        //绘制
        canvas.drawXX(xx);
        ...
    }

如上所示,创建一个Canvas与Bitmap,并将两者关联起来。最后调用Canvas绘制API,绘制的结果将保存在Bitmap里。这个过程实际上也是软件缓存绘制使用的方法。
当然拿到了Bitmap后,我们想让其展示就比较简单了,只要让其关联到View上就可以展示到屏幕上了。关联到View上实际上就是使用View关联的Canvas将生成的Bitmap绘制其上,

绘制流程全家福

用图表示绘制流程:

单纯的软件绘制与硬件加速绘制:

image.png

设置了软件缓存时的绘制:

image.png

至此,Draw过程系列文章结束。 本文源码基于Android 10。