写点什么

Android 性能优化 _ 大图做帧动画卡?优化帧动画之 SurfaceView 滑动窗口式帧复用

用户头像
Android架构
关注
发布于: 3 小时前

}}


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


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

生产者 & 消费者

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


public class FrameSurfaceView extends BaseSurfaceView {...//解析队列:存放已经解析帧素材 private LinkedBlockingQueue<Bitmap> 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<Integer> bitmapIds;private BitmapFactory.Options options;


public DecodeRunnable(int index, List<Integer> bitmapIds, BitmapFactory.Options options) {this.index = index;this.bitmapIds = bitmapIds;this.options = options;}


@Overridepublic 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<Integer> bitmapIds = new ArrayList<>();//大小为 3 的解析队列 private LinkedBlockingQueue<Bitmap> decodedBitmaps = new LinkedBlockingQueue<>(3);


//传入帧动画素材 public void setBitmapIds(List<Integer> 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++;}


而现在解码和绘制并发进行,且有多帧能被复用。这时就需要一个队列来维护可被复用的帧。


当绘制线程从解析队列头部取出帧图片并完成绘制后,该帧就可以被复用了,应该将其加入到复用队列队头。而解码线程在解码新的一帧图片之前,应该从复用队列的队尾取出可复用的帧。


一帧图片就这样在两个队列之间转圈。通过这样一个周而复始的循环,就可以将内存占用控制在有限范围内(解码队列长度*帧大小)。新增复用队列代码如下:


public class FrameSurfaceView extends BaseSurfaceView {//复用队列 private LinkedBlockingQueue<Bitmap> drawnBitmaps = new LinkedBlockingQueue<>(3);


//将已绘制图片存入复用队列 private void putDrawnBitmap(Bitmap bitmap) {drawnBitmaps.offer(bitmap);}


//从复用队列中取图片 private LinkedBitmap getDrawnBitmap() {Bitmap bitmap = null;try {bitmap = drawnBitmaps.take();} catch (InterruptedException e) {e.printStackTrace();}return bitmap;}


//复用上一帧解析下一帧并入解析队列 private void putDecodedBitmapByReuse(int resId, BitmapFactory.Options options) {Bitmap bitmap = getDrawnBitmap();options.inBitmap = bitmap;putDecodedBitmap(resId, options);}


private void drawOneFrame(Canvas canvas) {Bitmap bitmap = getDecodedBitmap();if (bitmap != null) {canvas.drawBitmap(bitmap, srcRect, dstRect, paint);}//帧绘制完毕后将其存入复用队列 putDrawnBitmap(bitmap);frameIndex++;}


private class DecodeRunnable implements Runnable {private int index;private List<Integer> bitmapIds;private BitmapFactory.Options options;


public DecodeRunnable(int index, List<Integer> bitmapIds, BitmapFactory.Options options) {this.index = index;this.bitmapIds = bitmapIds;this.options = options;}


@Overridepublic void run() {//在解析线程复用上一帧并解析下一帧存入解析队列 putDecodedBitmapByReuse(bitmapIds.get(index), options);index++;if (index < bitmapIds.size()) {handler.post(this);} else {index = 0;}}}}


  • 绘制帧完成后将其存入复用队列时使用了不带阻塞的offer(),这是为了避免慢速解析拖累快速绘制:假设复用队列已满,但解析线程还未完成当前解析,此时完成了一帧的绘制,并正在向复用队列存帧,若采用阻塞方法,则绘制线程因慢速解析而被阻塞。

  • 解析线程从复用队列获取复用帧时使用了阻塞的take(),这是为了避免快速解析导致内存溢出:假设复用队列为空,但绘制线程还未完成当前帧的绘制,此时解析线程完成了一帧的解析,并正在向复用队列取帧,若不采取阻塞方法,则解析线程复用帧失败,一块新的内存被申请用于存放解析出来的下一帧。


满怀期待运行代码并打开内存监控~~,内存没有膨胀,播了好几次也没有!动画也很流畅!


正打算庆祝的时候,内存监控中的一个对象引起了我的注意。

仅仅是播放了 5-6 次动画,就产生了 600+个实例,而Bitmap对象只有 3 个。

更蹊跷的是 600 个对象的内存占用和 3 个Bitmap的几乎相等。

仔细观察这 600 个对象,其中只有 3 个对象Retained size非常大,其余大小都是 16k。

点开这 3 个对象的成员后发现,每个对象都持有 1 个Bitmap

而且这个对象的名字叫LinkedBlockingQueue@Node

真相大白!


在向阻塞队列插入元素的时候,其内部会新建一个Node结点用于包裹插入元素,以offer()为例:


public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {public boolean offer(E e) {if (e == null) throw new NullPointerException();final AtomicInteger count = this.count;if (count.get() == capacity)return false;int c = -1;//新建结点 Node<E> node = new Node<E>(e);final ReentrantLock putLock = this.putLock;putLock.lock();try {if (count.get() < capacity) {enqueue(node);c = count.getAndIncrement();if (c + 1 < capacity)notFull.signal();}} finally {putLock.unlock();}if (c == 0)signalNotEmpty();return c >= 0;}}


突然想到了 Android 中的消息队列,消息被处理后放入消息池,构建新消息时会先从池中获取,以此实现消息的复用。消息机制中也维护了两个队列,一个是消息队列,一个是消息回收队列,两个队列之间形成循环,和本文中的场景非常相似。


为啥消息队列不会产生这么多冗余对象?


原因就在于LinkedBlockingQueue默默为我们包了一层结点,但我们并没有能力处理这层额外的结点。


抓狂中~~~,只要用LinkedBlockingQueue就必然会新建结点。。。要不就不用它吧。。。但不用它,实现生产者消费者就比较麻烦。。。还是得用。。。


无奈之下,只能使用复制粘贴大法,重写了一个自己的LinkedBlockingQueue并删除那句new Node<E>(),为简单起见,只列举了其中的put(),代码如下:


public class LinkedBlockingQueue {private final AtomicInteger count = new AtomicInteger();private final ReentrantLock takeLock = new ReentrantLock();private final Condition notEmpty = takeLock.newCondition();private final ReentrantLock putLock = new ReentrantLock();private final Condition notFull = putLock.newCondition();private final int capacity;private LinkedBitmap head;private LinkedBitmap tail;


public LinkedBlockingQueue(int capacity) {if (capacity <= 0) throw new IllegalArgumentException();this.capacity = capacity;}


public void put(LinkedBitmap bitmap) throws InterruptedException {if (bitmap == null) throw new NullPointerException();int c = -1;fina


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


l ReentrantLock putLock = this.putLock;final AtomicInteger count = this.count;putLock.lockInterruptibly();try {while (count.get() == capacity) {notFull.await();}enqueue(bitmap);c = count.getAndIncrement();if (c + 1 < capacity)notFull.signal();} finally {putLock.unlock();}if (c == 0)

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android性能优化 _ 大图做帧动画卡?优化帧动画之 SurfaceView滑动窗口式帧复用