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

2022-06-30  本文已影响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;
            }
        }
    }
} 

滑动窗口机制 & 预解析

为了让速度不同的生产者和消费者更流畅的协同工作,必须为速度较快的一方提供缓冲。
就好像 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++;
}  
上一篇下一篇

猜你喜欢

热点阅读