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

作者: 小鱼人爱编程 | 来源:发表于2021-11-11 13:10 被阅读0次

    前言

    Draw 过程系列文章

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

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

    软件绘制流程

    上篇说过在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。

    相关文章

      网友评论

        本文标题:Android 自定义View之Draw过程(下)

        本文链接:https://www.haomeiwen.com/subject/wkawzltx.html