IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android性能优化 _ 大图做帧动画卡?优化帧动画之 SurfaceView滑动窗口式帧复用 -> 正文阅读

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

}

protected abstract void onFrameDraw(Canvas canvas);
}

//帧动画绘制类:将绘制内容具体化为一张Bitmap
public class FrameSurfaceView extends BaseSurfaceView {

private BitmapFactory.Options options;

@Override
protected void onFrameDraw(Canvas canvas) {
clearCanvas(canvas);
if (!isStart()) {
return;
}
if (!isFinish()) {
//绘制一帧
drawOneFrame(canvas);
} else {
onFrameAnimationEnd();
}
}

private void drawOneFrame(Canvas canvas) {
//解析帧
frameBitmap = BitmapFactory.decodeResource(getResources(), bitmaps.get(bitmapIndex), options);
//复用帧
options.inBitmap = frameBitmap;
//绘制帧
canvas.drawBitmap(frameBitmap, srcRect, dstRect, paint);
bitmapIndex++;
}

}

对比图片解析速度

对于素材在 100k 以下的帧动画,上一篇的逐帧解析方案完全能够胜任。但如果素材是几百k,时间性能就不如预期。

掘友“小前锋”问:“你的方案有测试过大图吗?比如1024*768px”

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

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

于是乎对比了各种图片解码的速度,其中包括BitmapFactory.decodeStream()BitmapFactory.decodeResource()、并分别将图片放到res/rawres/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 =

《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》

【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享

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();

  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-12-05 12:09:21  更:2021-12-05 12:11:09 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 6:29:02-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码