android双缓冲绘图技术分析

作者: wensefu | 来源:发表于2017-03-24 03:06 被阅读3098次

    双缓冲、多缓冲技术在计算机科学中其实是一个广义的概念,不过其本质上的意思都是差不多的。今天我们就来讲一讲双缓冲技术在android绘图中的应用。

    何谓缓冲?

    在理解双缓冲的原理之前,我们先要明白,什么叫缓冲?
    我们可以举一个比较通俗的粟子,比如:

    工头给你一个任务,让你把50块大板砖从A处搬到距离你1000米之外的B处去。你心想,50块板砖? 小case,我一次就能扛完。于是你撸起袖子,一步一步,真的一趟就搞定了。这个时候工头一声奸笑对你说,小伙子不错,那边还有2000块砖,你也搬过去吧。。。
    看到这堆积如山的砖头,你眼前一黑,这孙子真是想累死我啊,钱给这么少还干这么多活!
    可是没办法呀,谁叫自己当初书读得少,长大了只能靠搬砖为生呢。正当你准备徒手一趟一趟地开始干时,丰满漂亮的工头,的老婆来了,她走过来,带着迷一般的微笑。那种笑容,甜蜜优雅,仿佛春风拂过泸沽湖,秋雨浸润九寨沟,让你虎躯一震。她对你说,你开工地上的卡车把这些砖搬过去吧,2000块砖太多了,一趟一趟搬太累。
    你瞬间来了精神,把砖搬到卡车上,油门一踩不带走一片云彩,一下就把2000块砖搬过去了。。。
    ............
    “快起来,什么时候了还在睡,快去搬砖!”
    该死的工头又来催!

    缓冲的概念就讲到这里。

    Android绘图中的双缓冲

    我们知道,我们在绘图时有两样东西是少不了的,一个是Canvas(画布),一个是Paint(画笔)。Canvas提供画各种图形的方法,如画圆(drawCircle),画矩形(drawRect)等等,Paint用来设置画笔的样式,比如笔的粗细,颜色等。每个Canvas内部持有一个Bitmap对象的引用,画图的过程其实就是往这个Bitmap当中写入ARGB信息。
    比如我们现在自定义一个View,在上面画一个矩形和一个圆:

    @Override
        protected void onDraw(Canvas canvas) {
            canvas.drawRect(rect,mPaint);
            canvas.drawCircle(cx,cy,100,mPaint);
        }
    

    那么现在有一个问题,画矩形和画圆是两个独立的动作,会不会在drawRect执行完之后屏幕上马上就会显示出来一个矩形呢?
    为了验证我们的猜想,我们在两个绘图动作中加一个sleep:

    @Override
        protected void onDraw(Canvas canvas) {
            canvas.drawRect(rect,mPaint);
            try {
                TimeUnit.MILLISECONDS.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            canvas.drawCircle(cx,cy,100,mPaint);
        }
    

    我们会看到,并不是先显示矩形再显示圆,而是两个几乎同时一起显示出来的。这就说明必须要等onDraw方法执行完成之后,才会把数据交给GPU去处理展示。这就是android绘图当中的第一道缓冲,即显示缓冲区。

    而所谓的双缓冲,在android绘图中其实就是再创建一个Canvas和对应的Bitmap,然后在onDraw方法里默认的Canvas通过drawBitmap画刚才new的那个bitmap从而实现双缓冲。用代码简单的表述是这样的:

    private void init(){
        Bitmap bufferBm = Bitmap.create(getWidth,getHeight,Bitmap.Config.ARGB_8888);
        Canvas bufferCanvas = new Canvas(bufferBm);
    }
    
    private void drawSomething(){
        bufferCanvas.drawRect();
        bufferCanvas.drawCircle();
        ...
        invalidate();
    }
    
    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawBitmap(bufferBm,0,0,null);
    }
    

    示意图:

    双缓冲绘图的优缺点及适用场景

    我们通过一个例子来说明。
    实现这样一个功能,一个自定义View,每次点击的时候在点击处画一个圆。我们先不使用双缓冲来实现:

    不用双缓冲的代码:

    public class MyView extends View{
    
        private Paint mPaint;
        private List<Point> mPoints;
    
        public MyView(Context context) {
            super(context);
        }
    
        public MyView(Context context, AttributeSet attrs) {
            super(context, attrs);
            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setColor(Color.GREEN);
            setBackgroundColor(Color.WHITE);
            mPoints = new ArrayList<>();
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            int action = event.getAction();
            switch (action){
                case MotionEvent.ACTION_DOWN:
                    mPoints.add(new Point((int)event.getX(),(int)event.getY()));
                    break;
                case MotionEvent.ACTION_UP:
                    invalidate();
                    break;
            }
            return true;
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            for (Point p : mPoints) {
                canvas.drawCircle(p.x,p.y,50,mPaint);
            }
        }
    

    在实验之前,我们先打开开发者选项里的”GPU呈现模式分析“,设置为“在屏幕上显示为条形图”(不同的手机可能有略微的差异,我这里用的是google Nexus5)。

    可以看到,当画的圆数目比较少时,GPU的负荷较低,但是出现一个逐步上升的趋势:

    不使用双缓冲-少数据-GPU占用图

    内存使用情况是这样的:


    不使用双缓冲-少数据-内存占用图

    当画的圆数目增加到比较大时,GPU负荷有点惨不妨睹了:


    不使用双缓冲-大数据-GPU占用图

    这时的内存使用情况:


    不使用双缓冲-大数据-内存占用图

    我们现在改用双缓冲来绘图,代码如下:

    public class MyView extends View{
    
        private Paint mPaint;
        private Canvas mBufferCanvas;
        private Bitmap mBufferBitmap;
    
        public MyView(Context context) {
            super(context);
        }
    
        public MyView(Context context, AttributeSet attrs) {
            super(context, attrs);
            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setColor(Color.GREEN);
            setBackgroundColor(Color.WHITE);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            int action = event.getAction();
            switch (action){
                case MotionEvent.ACTION_DOWN:
                    if (mBufferBitmap == null) {
                        mBufferBitmap = Bitmap.createBitmap(getWidth(),getHeight(), Bitmap.Config.ARGB_8888);
                        mBufferCanvas = new Canvas(mBufferBitmap);
                    }
                    mBufferCanvas.drawCircle((int)event.getX(),(int)event.getY(),50,mPaint);
                    break;
                case MotionEvent.ACTION_UP:
                    invalidate();
                    break;
            }
            return true;
        }
    
        @Override
        protected void onDraw(Canvas canvas) {
            if (mBufferBitmap == null) {
                return;
            }
            canvas.drawBitmap(mBufferBitmap,0,0,null);
        }
    }
    

    使用双缓冲,在数量较小时的GPU使用情况是这样的:


    使用双缓冲-少数据-GPU占用图

    这时候的内存使用情况:


    使用双缓冲-少数据-内存占用图

    使用双缓冲,在数量非常大的时候,GPU使用情况是这样的:

    使用双缓冲-大数据-GPU占用图

    内存使用情况:


    使用双缓冲-大数据-内存占用图

    从上面的实验数据我们可以得出结论:

    • 在绘制数据量较小时,不使用双缓冲,GPU的负荷更低,即绘制性能更高;
    • 在绘制数据量较大时,使用双缓冲绘图,绘制性能明显高于不使用双缓冲的情况;
    • 使用双缓冲会增加内存消耗。

    其实上面的结论也很好理解,就像上面举的搬砖的例子,如果砖少的话,用车来拉明显是划不来的,砖的数量很多的时候,用车来拉就可以节省很多时间,但是用车就要消耗额外的资源,这就需要根据不同的情况做出正确的选择。

    android的双缓冲绘图技术就讲到这里,有不对的地方或大家有什么问题欢迎留言。


    android实现画板功能源码

    转载请说明出处:http://www.jianshu.com/p/efc0bebfd22e

    相关文章

      网友评论

      • ZanderZhan:我自己做的实验得出的结论:
        在绘制数量较小时,使用双缓冲和使用单缓冲性能相差不大。
      • ZanderZhan:从你的贴图来看,结论的第1点不成立啊
        绘制数量较小,使用双缓冲的GPU的性能也是比单缓冲要好
      • 我为世界和平:没用双缓冲的代码,每次松手调用invalidate,导致onDraw再次调用,因为mPoints中的Point是逐渐增加的,这样,触摸的地方越多,最后重复绘制的点越多。刚开始一次绘制一个点,后来就绘制两个点有一个重复的,下次绘制三个点,有两个重复的,当绘制第一百个点时,前面99个又重复了,最开始点击的那个点会被重复绘制100次,这样写导致的GPU升高,个人感觉并不是因为没有使用双缓冲
        奋斗小青年Jerome:应该是前面99个点在集合里面,松开手调用invalidate的时候,canvas画布不会保存之前绘制的点(不会一个点一层canvas这样堆叠起来,类似Framlayout),而是会将之前canvas画的那99个点全部清理掉,重新绘制100个点
        学海无涯2016:说错了吧。第二次画会把第一次的清空。所以绘制100次,前面99个不重复。
      • 飞天德先生:勉勉强强能理解作者想要表达的是什么意思吧,但总体感觉说的有些冗余,但挺有用的一个知识
      • 93e1d8feb3d0:哈哈哈,确实那个栗子是一脸黑人问号,从鸿阳那过来的~
      • dce37a529186:我认为你两个对比不科学, 科学的对比 ,大的变量只能有一个,但是你却有两个变量。 1. 将绘制一系列点操作 变为 绘制图片 ( canvas.drawBitmap(mBufferBitmap,0,0,null);) 2. 绘制图片 从 每次点击绘制一系列点 变为 增加 一个点( mBufferCanvas.drawCircle((int)event.getX(),(int)event.getY(),50,mPaint);). 感觉毫无说服力
        学海无涯2016:@学海无涯2016 应该是通过drawBitmap往mBufferBitmap上添加了一个。然后drawBitmap。 刚也理解错了。
        学海无涯2016:@黄禹HY 正常绘制1000个点和使用drawBitmap绘制1000个点。 每次刷新,都是要绘制1000.而不是drawBitmap只绘制最后一个吧。
        dce37a529186:你这个只能算局部刷新 和 全部刷新的区别。 那个mBufferBitmap 就属于背景
      • 李斯维:写的很好,但就是那个比较通俗的栗子看的我一脸黑人问号。另外问下如果一个自定义 view 必须使用双缓冲的话,换用 textureview 或 Surfaceview 会不会更合适呢?
      • Cyandev:Android 之后引入的硬件加速,所有 Canvas 操作都只生成相应的 DisplayList,其实并不能算一层缓冲,软件绘制才有缓冲区,在一次事件循环后把位图 BitBlt 到显存中。

      本文标题:android双缓冲绘图技术分析

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