Android性能优化 _ 大图做帧动画卡?优化帧动画之 SurfaceView滑动窗口式帧复用
作者:互联网
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++;
}
而现在解码和绘制并发进行,且有多帧能被复用。这时就需要一个队列来维护可被复用的帧。
当绘制线程从解析队列头部取出帧图片并完成绘制后,该帧就可以被复用了,应该将其加入到复用队列队头。而解码线程在解码新的一帧图片之前,应该从复用队列的队尾取出可复用的帧。
一帧图片就这样在两个队列之间转圈。通过这样一个周而复始的循环,就可以将内存占用控制在有限范围内(解码队列长度*帧大小)。新增复用队列代码如下:
public class FrameSurfaceView extends BaseSurfaceView {
//复用队列
private LinkedBlockingQueue 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 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() {
//在解析线程复用上一帧并解析下一帧存入解析队列
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 extends AbstractQueue implements BlockingQueue, 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 node = new Node(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;
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
notFull.await();
}
enqueue(bitmap);
c = count.getAndIncrement();
最后我还整理了很多Android中高级的PDF技术文档。以及一些大厂面试真题解析文档。需要的朋友都可以点击GitHub直接获取方式
Android高级架构师之路很漫长,一起共勉吧!
术文档。以及一些大厂面试真题解析文档。需要的朋友都可以点击GitHub直接获取方式**
[外链图片转存中…(img-XlOEan2e-1644033032141)]
Android高级架构师之路很漫长,一起共勉吧!
标签:动画,队列,大图,复用,private,线程,解析,优化,options 来源: https://blog.csdn.net/m0_66264324/article/details/122789153