美文网首页
Android性能优化15 --- 大图做帧动画卡?优化帧动画之

Android性能优化15 --- 大图做帧动画卡?优化帧动画之

作者: 沪漂意哥哥 | 来源:发表于2022-06-30 08:53 被阅读0次

    对比图片解析速度

    对于素材在 100k 以下的帧动画,上一篇的逐帧解析方案完全能够胜任。但如果素材是几百k,时间性能就不如预期。
    掘友“小前锋”问:“你的方案有测试过大图吗?比如1024768px”*

    在逐帧解析SurfaceView上试了下这个大小的帧动画,虽然播放过程很连续,但 600ms 的帧动画被放成了 1s。因为预定义的每帧播放时间被解码时间拉长了。

    有没有比BitmapFactory.decodeResource()更快的解码方式?

    于是乎对比了各种图片解码的速度,其中包括BitmapFactory.decodeStream()、BitmapFactory.decodeResource()、并分别将图片放到res/raw、res/drawable、及assets,还在 GitHub 上发现了RapidDecoder这个库(兴奋不已!)。自定义了测量函数执行时间的工具类:

    public class MethodUtil {
      //测量并打印单次函数执行耗时
      public static long time(Runnable runnable) {
      long start = SystemClock.elapsedRealtime();
      runnable.run();
      long end = SystemClock.elapsedRealtime();
      long span = end - start;
      Log.v(“ttaylor”, “MethodUtil.time()” + " time span = " + span + " ms");
      return span;
      }
    }
    
    public class NumberUtil {
      private static long total;
      private static int times;
      private static String tag;
    
      //统计并打印多次执行时间的平均值
      public static void average(String tag, Long l) {
        if (!TextUtils.isEmpty(tag) && !tag.equals(NumberUtil.tag)) {
          reset();
          NumberUtil.tag = tag;
      }
      times++;
      total += l;
      int average = total / times ;
      Log.v(“ttaylor”, "Average.average() " + NumberUtil.tag + " average = " + average);
    }
    
      private static void reset() {
        total = 0;
        times = 0;
      }
    } 
    

    经多次测试取平均值,执行时间最长的是BitmapFactory.decodeResource(),最短的是用BitmapFactory.decodeStream()解析assets图片,后者只用了前者一半时间。而RapidDecoder库的时间介于两者之间(失望至极~),不过它提供了一种边解码边绘制的技术号称比先解码再绘制要快,还没来得及试。

    虽然将解码时间减半了,但解码一张 1MB 图片还是需要 60+ms,仍不能满足时间性能要求。

    独立解码线程

    现在的矛盾是 图片解析速度 慢于 图片绘制速度,如果解码和绘制在同一个线程串行的进行,那解码势必会拖慢绘制效率。
    可不可以将解码图片放在一个单独的线程中进行?
    在上一篇FrameSurfaceView的基础上新增了独立解码线程:

    public class FrameSurfaceView extends BaseSurfaceView {
        …
        //独立解码线程
        private HandlerThread decodeThread;
        //解码算法写在这里面
        private DecodeRunnable decodeRunnable;
    
        //播放帧动画时启动解码线程
        public void start() {
            decodeThread = new HandlerThread(DECODE_THREAD_NAME);
            decodeThread.start();
            handler = new Handler(decodeThread.getLooper());
            handler.post(decodeRunnable);
        }
    
        private class DecodeRunnable implements Runnable {
    
            @Override
            public void run() {
                //在这里解码
            }
        }
    } 
    

    这样一来,基类中有独立的绘制线程,而子类中有独立的解码线程,解码速度不再影响绘制速度。

    新的问题来了:图片被解码后存放在哪里?

    生产者 & 消费者

    存放解码图片的容器,会被两个线程访问,绘制线程从中取图片(消费者),解码线程往里存图片(生产者),需考虑线程同步。第一个想到的就是LinkedBlockingQueue,于是乎在FrameSurfaceView中新增了大小为 1 的阻塞队列及存取操作:

    public class FrameSurfaceView extends BaseSurfaceView {
        …
        //解析队列:存放已经解析帧素材
        private LinkedBlockingQueue decodedBitmaps = new LinkedBlockingQueue<>(1);
        //记录已绘制的帧数
        private int frameIndex ;
    
        //存解码图片
        private void putDecodedBitmap(int resId, BitmapFactory.Options options) {
            Bitmap bitmap = decodeBitmap(resId, options);
            try {
                decodedBitmaps.put(bitmap);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    
        //取解码图片
        private Bitmap getDecodedBitmap() {
            Bitmap bitmap = null;
            try {
                bitmap = decodedBitmaps.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return bitmap;
        }
    
        //解码图片
        private Bitmap decodeBitmap(int resId, BitmapFactory.Options options) {
            options.inScaled = false;
            InputStream inputStream = getResources().openRawResource(resId);
            return BitmapFactory.decodeStream(inputStream, null, options);
        }
    
        private void drawOneFrame(Canvas canvas) {
            //在绘制线程中取解码图片并绘制
            Bitmap bitmap = getDecodedBitmap();
            if (bitmap != null) {
            canvas.drawBitmap(bitmap, srcRect, dstRect, paint);
            }
            frameIndex++;
        }
    
        private class DecodeRunnable implements Runnable {
            private int index;
            private List bitmapIds;
            private BitmapFactory.Options options;
    
            public DecodeRunnable(int index, List bitmapIds, BitmapFactory.Options options) {
                this.index = index;
                this.bitmapIds = bitmapIds;
                this.options = options;
            }
    
            @Override
            public void run() {
                //在解码线程中解码图片
                putDecodedBitmap(bitmapIds.get(index), options);
                index++;
                if (index < bitmapIds.size()) {
                    handler.post(this);
                } else {
                    index = 0;
                }
            }
        }
    } 
    
    • 绘制线程在每次绘制之前调用阻塞的take()从解析队列的队头拿帧图片,解码线程不断地调用阻塞的put()往解析队列的队尾存帧图片。
    • 虽然assets目录下的图片解析速度最快,但res/raw目录的速度和它相差无几,为了简单起见,这里使用了openRawResource读取res/raw中的图片。
    • 虽然解码和绘制分别在不同线程,但如果存放解码图片容器大小为 1 ,绘制进程必须等待解码线程,绘制速度还是会被解码速度拖累,看似互不影响的两个线程,其实相互牵制。

    滑动窗口机制 & 预解析

    为了让速度不同的生产者和消费者更流畅的协同工作,必须为速度较快的一方提供缓冲。
    就好像 TCP 拥塞控制中的滑动窗口机制,发送方产生报文的速度快于接收方消费报文的速度,遂发送方不必等收到前一个报文的确认再发送下一个报文。

    对于当前 case ,需要将存放图片容器增大,并在帧动画开始前预解析前几帧存入解析队列。

    public class FrameSurfaceView extends BaseSurfaceView {
        …
        //下一个该被解析的素材索引
        private int bitmapIdIndex;
        //帧动画素材容器
        private List bitmapIds = new ArrayList<>();
        //大小为3的解析队列
        private LinkedBlockingQueue decodedBitmaps = new LinkedBlockingQueue<>(3);
    
        //传入帧动画素材
        public void setBitmapIds(List bitmapIds) {
            if (bitmapIds == null || bitmapIds.size() == 0) {
                return;
            }
            this.bitmapIds = bitmapIds;
            preloadFrames();
        }
    
        //预解析前几帧
        private void preloadFrames() {
            //解析一帧并将图片入解析队列
            putDecodedBitmap(bitmapIds.get(bitmapIdIndex++), options);
            putDecodedBitmap(bitmapIds.get(bitmapIdIndex++), options);
        }
    } 
    

    独立解码线程、滑动窗口机制、预加载都已 code 完毕。运行一把代码(坐等惊喜~)。

    居然流畅的播起来了!兴奋的我忍不住播了好几次。。。打开内存监控一看(头顶竖下三条线),一夜回到解放前:每播放一次,内存中就会新增 N 个Bitmap对象(N为帧动画总帧数)。

    原来重构过程中,将解码时的帧复用逻辑去掉了。当前 case 中,帧复用也变得复杂起来。

    复用队列

    当解码和绘制是在一个线程中串行进行,且只有一帧被复用,只需这样写代码就能实现帧复用:

    private void drawOneFrame(Canvas canvas) {
        frameBitmap = BitmapFactory.decodeResource(getResources(), bitmaps.get(bitmapIndex), options);
        //复用上一帧Bitmap的内存
        options.inBitmap = frameBitmap;
        canvas.drawBitmap(frameBitmap, srcRect, dstRect, paint);
        bitmapIndex++;
    }  
    

    相关文章

      网友评论

          本文标题:Android性能优化15 --- 大图做帧动画卡?优化帧动画之

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