Android性能优化 _ 大图做帧动画卡?优化帧动画之 SurfaceView滑动窗口式帧复用
(ps:粗斜体表示引导方案逐步进化的关键点)
SurfaceView逐帧解析 & 帧复用
简单回顾下上一篇的内容:原生帧动画在播放前解析所有帧,对内存压力大。SurfaceView
可以精细地控制帧动画每一帧的绘制,在每一帧绘制前才解析当前帧,且解析后续帧时复用前帧内存空间。遂整个过程在内存只申请了一帧图片大小的空间。下面罗列了一些关键代码:
基类:定义绘制框架
public abstract class BaseSurfaceView extends SurfaceView implements SurfaceHolder.Callback {
…
//绘制线程
private HandlerThread handlerThread;
private Handler handler;
@Override
public void surfaceCreated(SurfaceHolder holder) {
startDrawThread();
}
private void startDrawThread() {
handlerThread = new HandlerThread(“SurfaceViewThread”);
handlerThread.start();
handler = new Handler(handlerThread.getLooper());
handler.post(new DrawRunnable());
}
private class DrawRunnable implements Runnable {
@Override
public void run() {
try {
canvas = getHolder().lockCanvas();
//绘制一帧,包括解码+绘制帧
onFrameDraw(canvas);
} catch (Exception e) {
e.printStackTrace();
} finally {
getHolder().unlockCanvasAndPost(canvas);
onFrameDrawFinish();
}
//若onFrameDraw()执行超时,会导致下一帧的绘制被推后,预定的帧时间间隔不生效
handler.postDelayed(this, frameDuration);
}
}
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/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++;
}
最后笔者收集整理了一份Flutter高级入门进阶资料PDF
以下是资料目录和内容部分截图
里面包括详细的知识点讲解分析,带你一个星期入门Flutter。还有130个进阶学习项目实战视频教程,让你秒变大前端。学不会来打我!
(frameBitmap, srcRect, dstRect, paint);
bitmapIndex++;
}
最后笔者收集整理了一份Flutter高级入门进阶资料PDF
以下是资料目录和内容部分截图
[外链图片转存中…(img-SQVsRd8e-1644995337753)]
[外链图片转存中…(img-YB268ikc-1644995337754)]
里面包括详细的知识点讲解分析,带你一个星期入门Flutter。还有130个进阶学习项目实战视频教程,让你秒变大前端。学不会来打我!
[外链图片转存中…(img-fVuNRmEc-1644995337755)]
以上资料皆无偿分享,领取方式:点击我的GitHub即可免费获取
Android性能优化 _ 大图做帧动画卡?优化帧动画之 SurfaceView滑动窗口式帧复用相关推荐
- 整站SEO优化需要怎么做?SEO整站优化的思路及步骤
整站SEO优化需要怎么做?整站SEO优化为使网站全体各各细节到达很优的作用,不扔掉任何有关于网站事务的长尾要害词,高掩盖方针客户集体,有层次的定位网站要害词. 和要害词排名不同的时,单纯地进犯几个要害 ...
- android 属性翻牌动画,Android自定义动画--卡牌翻牌动画
Android系统中自带了四种动画,但是都只是平面上的并不能实现我们很常见的翻牌动画,所以今天我们就要通过自定义动画来实现翻牌动画. 要实现翻牌动画,我们需要了解三个类,一个是matrix类,一个是c ...
- tensorflow超参数优化_机器学习模型的超参数优化
引言 模型优化是机器学习算法实现中最困难的挑战之一.机器学习和深度学习理论的所有分支都致力于模型的优化. 机器学习中的超参数优化旨在寻找使得机器学习算法在验证数据集上表现性能最佳的超参数.超参数与一般 ...
- java程序性能优化_怎么做JAVA程序性能优化
展开全部 1)尽量指定类.方62616964757a686964616fe59b9ee7ad9431333433623731法的final修饰符.带有final修饰符的类是不可派生的,Java编译器会 ...
- Vue动画卡顿,动画帧数优化
项目中需要在地图中做一个风场粒子动画,原生js方法绘制的动画非常流畅,但是一放到Vue中就会变得很卡顿,帧数大概只有原来的30%,最终发现是因为map变量放在了date中重写了变量的getter/se ...
- mysql提高性能 硬件_高性能MySQL–操作系统和硬件优化
许多不同的硬件都可以影响MySQL的性能,但我们认为最常见的两个瓶颈是CPU和I/O资源.当数据可以放在内存中或者可以从磁盘中以足够快的速度读取时,CPU可能出现瓶颈.另一方I/O瓶颈一般发生在工作所 ...
- mysql sql优化_浅谈mysql中sql优化
说到sql优化,一般有几个步骤呢,在网上看到了一篇很不错的帖子.在这分享一下吧,也是自己学习的一个过程. 一.查找慢查询 1.1.查看SQL执行频率 SHOW STATUS LIKE 'Com_%'; ...
- delete优化_深入理解JIT和编译优化
点击上方的蓝字关注我吧 程序那些事 简介 小师妹已经学完JVM的简单部分了,接下来要进入的是JVM中比较晦涩难懂的概念,这些概念是那么的枯燥乏味,甚至还有点惹人讨厌,但是要想深入理解JVM,这些概念是 ...
- mysql 连接 优化_(一)MySQL 连接优化
1.查看连接参数(show variables) mysql> show variables like '%connect%'; +------------------------------- ...
最新文章
- 加密界又一响声:WhatsApp宣布对所有通讯信息进行端到端加密
- C# - 基于LinkLabel可动态生成多超链接信息的自定义控件
- 大学物理规范作业25稳恒磁场_山东一地出台规定:严禁家长代批作业,违反规定将被一票否决...
- 请写出至少五个块级元素_html 行级元素和块级元素标签列表分别有哪些
- “AI捡垃圾”上热搜了!46城垃圾分类将投200亿,你怎么看?
- php mysql 图像_将图像插入MySQL并使用PHP检索图像
- POJ3737 UmBasketella
- 开玩笑呢?学习KMP算法能改变自我认知? | 原力计划
- Linux中的文件权限
- GoLand 远程开发配置
- nod32更新服务无法设置问题更改
- springboot项目实现mqtt客户端
- 《云原生入门级开发者认证》学习笔记之云原生架构总览
- Flutter APNS device token not set before retrieving FCM Token for Sender ID
- 十年阿里巴巴资深架构师整理分享的SpringSecurity实战文档
- LCD1602与DHT11温湿度的使用
- js爬取今日头条头条号的文章
- Cloudsim和算法
- Ant Design中的表格中key的处理
- r5 6600h怎么样 相当于什么水平
热门文章
- 【读点论文】EfficientFormer: Vision Transformers at MobileNet Speed,运用纯transformer架构对比卷积模型在终端上部署的推理速度
- Rigify:面向初学者解决Rigify各种错误的入门级通用解决办法
- uva 10128 队伍
- 离散数学——数学结构
- 通用计算机的发展历程是巨型机大型机小型机,计组1——计算机系统概述
- STM32单片机语音声控智能台灯可调光冷暖光人检测锂电池供电太阳能和USB充电
- 射频百科:双工器是什么?双工器工作原理
- Arch Linux fcitx 新世纪五笔配置
- 关于在多重积分以及曲线曲面积分中对称性的应用
- 安腾处理器 oracle,英特尔展示下一代安腾处理器Poulson