andoid逐帧动画oom_帧动画内存OOM?不存在的!—— SurfaceView逐帧解析

2023-10-28

Android 提供了AnimationDrawable用于实现帧动画。在动画开始之前,所有帧的图片都被解析并占用内存,一旦动画较复杂帧数较多,在低配置手机上容易发生 OOM。即使不发生 OOM,也会对内存造成不小的压力。下面代码展示了一个帧数为4的帧动画:

原生帧动画

AnimationDrawable drawable = new AnimationDrawable();

drawable.addFrame(getDrawable(R.drawable.frame1), frameDuration);

drawable.addFrame(getDrawable(R.drawable.frame2), frameDuration);

drawable.addFrame(getDrawable(R.drawable.frame3), frameDuration);

drawable.addFrame(getDrawable(R.drawable.frame4), frameDuration);

drawable.setOneShot(true);

ImageView ivFrameAnim = ((ImageView) findViewById(R.id.frame_anim));

ivFrameAnim.setImageDrawable(drawable);

drawable.start();

有没有什么办法让帧动画的数据逐帧加载,而不是一次性全部加载到内存?SurfaceView就提供了这种能力。

SurfaceView

屏幕的显示机制和帧动画类似,也是一帧一帧的连环画,只不过刷新频率很高,感觉像连续的。为了显示一帧,需要经历计算和渲染两个过程,CPU 先计算出这一帧的图像数据并写入内存,然后调用 OpenGL 命令将内存中数据渲染成图像存放在 GPU Buffer 中,显示设备每隔一定时间从 Buffer 中获取图像并显示。

上述过程中的计算,对于View来说,就好比在主线程遍历 View树 以决定视图画多大(measure),画在哪(layout),画些啥(draw),计算结果存放在内存中,SurfaceFlinger 会调用 OpenGL 命令将内存中的数据渲染成图像存放在 GPU Buffer 中。每隔16.6ms,显示器从 Buffer 中取出帧并显示。所以自定义 View 可以通过重载onMeasure()、onLayout()、onDraw()来定义帧内容,但不能定义帧刷新频率。

SurfaceView可以突破这个限制。而且它可以将计算帧数据放到独立的线程中进行。下面是自定义SurfaceView的模版代码:

public abstract class BaseSurfaceView extends SurfaceView implements SurfaceHolder.Callback {

public static final int DEFAULT_FRAME_DURATION_MILLISECOND = 50;

//用于计算帧数据的线程

private HandlerThread handlerThread;

private Handler handler;

//帧刷新频率

private int frameDuration = DEFAULT_FRAME_DURATION_MILLISECOND;

//用于绘制帧的画布

private Canvas canvas;

private boolean isAlive;

public BaseSurfaceView(Context context) {

super(context);

init();

}

protected void init() {

getHolder().addCallback(this);

//设置透明背景,否则SurfaceView背景是黑的

setBackgroundTransparent();

}

private void setBackgroundTransparent() {

getHolder().setFormat(PixelFormat.TRANSLUCENT);

setZOrderOnTop(true);

}

@Override

public void surfaceCreated(SurfaceHolder holder) {

isAlive = true;

startDrawThread();

}

@Override

public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

}

@Override

public void surfaceDestroyed(SurfaceHolder holder) {

stopDrawThread();

isAlive = false;

}

//停止帧绘制线程

private void stopDrawThread() {

handlerThread.quit();

handler = null;

}

//启动帧绘制线程

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

if (!isAlive) {

return;

}

try {

//1.获取画布

canvas = getHolder().lockCanvas();

//2.绘制一帧

onFrameDraw(canvas);

} catch (Exception e) {

e.printStackTrace();

} finally {

//3.将帧数据提交

getHolder().unlockCanvasAndPost(canvas);

//4.一帧绘制结束

onFrameDrawFinish();

}

//不停的将自己推送到绘制线程的消息队列以实现帧刷新

handler.postDelayed(this, frameDuration);

}

}

protected abstract void onFrameDrawFinish();

protected abstract void onFrameDraw(Canvas canvas);

}

用HandlerThread作为独立帧绘制线程,好处是可以通过与其绑定的Handler方便地实现“每隔一段时间刷新”,而且在Surface被销毁的时候可以方便的调用HandlerThread.quit()来结束线程执行的逻辑。

DrawRunnable.run()运用模版方法模式定义了绘制算法框架,其中帧绘制逻辑的具体实现被定义成两个抽象方法,推迟到子类中实现,因为绘制的东西是多样的,对于本文来说,绘制的就是一张张图片,所以新建BaseSurfaceView的子类FrameSurfaceView:

逐帧解析 & 及时回收

public class FrameSurfaceView extends BaseSurfaceView {

public static final int INVALID_BITMAP_INDEX = Integer.MAX_VALUE;

private List bitmaps = new ArrayList<>();

//帧图片

private Bitmap frameBitmap;

//帧索引

private int bitmapIndex = INVALID_BITMAP_INDEX;

private Paint paint = new Paint();

private BitmapFactory.Options options = new BitmapFactory.Options();

//帧图片原始大小

private Rect srcRect;

//帧图片目标大小

private Rect dstRect = new Rect();

private int defaultWidth;

private int defaultHeight;

public void setDuration(int duration) {

int frameDuration = duration / bitmaps.size();

setFrameDuration(frameDuration);

}

public void setBitmaps(List bitmaps) {

if (bitmaps == null || bitmaps.size() == 0) {

return;

}

this.bitmaps = bitmaps;

//默认情况下,计算第一帧图片的原始大小

getBitmapDimension(bitmaps.get(0));

}

private void getBitmapDimension(Integer integer) {

final BitmapFactory.Options options = new BitmapFactory.Options();

options.inJustDecodeBounds = true;

BitmapFactory.decodeResource(this.getResources(), integer, options);

defaultWidth = options.outWidth;

defaultHeight = options.outHeight;

srcRect = new Rect(0, 0, defaultWidth, defaultHeight);

requestLayout();

}

public FrameSurfaceView(Context context) {

super(context);

}

@Override

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

super.onLayout(changed, left, top, right, bottom);

dstRect.set(0, 0, getWidth(), getHeight());

}

@Override

protected void onFrameDrawFinish() {

//在一帧绘制完后,直接回收它

recycleOneFrame();

}

//回收帧

private void recycleOneFrame() {

if (frameBitmap != null) {

frameBitmap.recycle();

frameBitmap = null;

}

}

@Override

protected void onFrameDraw(Canvas canvas) {

//绘制一帧前需要先清画布,否则所有帧都叠在一起同时显示

clearCanvas(canvas);

if (!isStart()) {

return;

}

if (!isFinish()) {

drawOneFrame(canvas);

} else {

onFrameAnimationEnd();

}

}

//绘制一帧,是张Bitmap

private void drawOneFrame(Canvas canvas) {

frameBitmap = BitmapUtil.decodeOriginBitmap(getResources(), bitmaps.get(bitmapIndex), options);

canvas.drawBitmap(frameBitmap, srcRect, dstRect, paint);

bitmapIndex++;

}

private void onFrameAnimationEnd() {

reset();

}

private void reset() {

bitmapIndex = INVALID_BITMAP_INDEX;

}

//帧动画是否结束

private boolean isFinish() {

return bitmapIndex >= bitmaps.size();

}

//帧动画是否开始

private boolean isStart() {

return bitmapIndex != INVALID_BITMAP_INDEX;

}

//开始播放帧动画

public void start() {

bitmapIndex = 0;

}

private void clearCanvas(Canvas canvas) {

paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));

canvas.drawPaint(paint);

paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));

}

}

FrameSurfaceView继承自BaseSurfaceView,所以它复用了基类的绘制框架算法,并且定了自己每一帧的绘制内容:一张Bitmap。

Bitmap资源 id 通过setBitmaps()传递进来, 绘制一帧解析一张 ,在每一帧绘制完毕后,调用Bitmap.recycle()释放图片 native 内存并去除 java 堆中图片像素数据的引用。这样当 GC 发生时,图片像素数据可以及时被回收。

一切都是这么地能够自圆其说,我迫不及待地运行代码并打开AndroidStudio的Profiler标签页,切换到MEMORY,想用真实内存数据验证下性能。但残酷的事实狠狠地打了下脸。。。多次播放帧动画后,内存占用居然比原生AnimationDrawable还大,而且每播放一次,内存中都会多出 N 个Bitmap对象(N为帧动画总帧数)。唯一令人欣慰的是,手动触发 GC 后帧动画图片能够被回收。(AnimationDrawable中的图片数据不会被 GC)

原因就在于自作聪明地及时回收,每一帧绘制完后帧数据被回收,那下一帧解析Bitmap时只能新申请一块内存。帧动画每张图片大小是一致的,是不是能复用上一帧Bitmap的内存空间?于是乎有了下面这个版本的FrameSurfaceView:

逐帧解析 & 帧复用

public class FrameSurfaceView extends BaseSurfaceView {

public static final int INVALID_BITMAP_INDEX = Integer.MAX_VALUE;

private List bitmaps = new ArrayList<>();

private Bitmap frameBitmap;

private int bitmapIndex = INVALID_BITMAP_INDEX;

private Paint paint = new Paint();

private BitmapFactory.Options options;

private Rect srcRect;

private Rect dstRect = new Rect();

public void setDuration(int duration) {

int frameDuration = duration / bitmaps.size();

setFrameDuration(frameDuration);

}

public void setBitmaps(List bitmaps) {

if (bitmaps == null || bitmaps.size() == 0) {

return;

}

this.bitmaps = bitmaps;

getBitmapDimension(bitmaps.get(0));

}

private void getBitmapDimension(Integer integer) {

final BitmapFactory.Options options = new BitmapFactory.Options();

options.inJustDecodeBounds = true;

BitmapFactory.decodeResource(this.getResources(), integer, options);

defaultWidth = options.outWidth;

defaultHeight = options.outHeight;

srcRect = new Rect(0, 0, defaultWidth, defaultHeight);;

}

public FrameSurfaceView(Context context) {

super(context);

}

@Override

protected void init() {

super.init();

//定义解析Bitmap参数为可变类型,这样才能复用Bitmap

options = new BitmapFactory.Options();

options.inMutable = true;

}

@Override

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

super.onLayout(changed, left, top, right, bottom);

dstRect.set(0, 0, getWidth(), getHeight());

}

@Override

protected int getDefaultWidth() {

return defaultWidth;

}

@Override

protected int getDefaultHeight() {

return defaultHeight;

}

@Override

protected void onFrameDrawFinish() {

//每帧绘制完毕后不再回收

// recycle();

}

public void recycle() {

if (frameBitmap != null) {

frameBitmap.recycle();

frameBitmap = null;

}

}

@Override

protected void onFrameDraw(Canvas canvas) {

clearCanvas(canvas);

if (!isStart()) {

return;

}

if (!isFinish()) {

drawOneFrame(canvas);

} else {

onFrameAnimationEnd();

}

}

private void drawOneFrame(Canvas canvas) {

frameBitmap = BitmapUtil.decodeOriginBitmap(getResources(), bitmaps.get(bitmapIndex), options);

//复用上一帧Bitmap的内存

options.inBitmap = frameBitmap;

canvas.drawBitmap(frameBitmap, srcRect, dstRect, paint);

bitmapIndex++;

}

private void onFrameAnimationEnd() {

reset();

}

private void reset() {

bitmapIndex = INVALID_BITMAP_INDEX;

}

private boolean isFinish() {

return bitmapIndex >= bitmaps.size();

}

private boolean isStart() {

return bitmapIndex != INVALID_BITMAP_INDEX;

}

public void start() {

bitmapIndex = 0;

}

private void clearCanvas(Canvas canvas) {

paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));

canvas.drawPaint(paint);

paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));

}

}

将Bitmap的解析参数inBitmap设置为已经成功解析的Bitmap对象以实现复用。

这一次不管重新播放多少次帧动画,内存中Bitmap数量只会增加1,因为只在解析第一张图片是分配了内存。而这块内存可以在FrameSurfaceView生命周期结束时手动调用recycle()回收。

talk is cheap, show me the code

为了更清晰的展示,上述代码段省略了一些和主题无关的自定义 View 细节,完整的代码可以点击这里。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

andoid逐帧动画oom_帧动画内存OOM?不存在的!—— SurfaceView逐帧解析 的相关文章

  • 弹性盒子(Flex Box)详解

    1 弹性盒子的特点 弹性盒子是 CSS3 的一种新的布局模式 CSS3 弹性盒 Flexible Box 或 flexbox 是一种当页面需要适应不同的屏幕大小以及设备类型时确保元素拥有恰当的行为的布局方式 主轴 main axis 是沿着
  • 回归分析学习与思考(2)

    课本上的东西 发不了链接 只能写原创了 先画散点图看看大概的模型plot x y 参数估计 最小二乘法 显著性检验 对参数进行t检验 对回归方程进行F检验 相关系数检验R square 参数区间估计 beta int B 函数 回归函数 B
  • vue项目前端解决跨域问题

    在前后端分离项目中 跨域是一定会出现的问题 本文主要介绍跨域问题的解决思路 以及在vue项目中如何使用代理的方式在前端解决跨域问题 同时提供一个后段解决的方案 1 产生原因 跨域问题产生的原因是浏览器的同源策略 浏览器同源策略是浏览器中的一
  • 数字千分位逗号隔开

    FUNCTION Z02FI NUMBER SPLIT Local Interface IMPORTING REFERENCE I NUM TYPE STRING EXPORTING REFERENCE E NUM TYPE STRING
  • 利用Hexo在Github上部署个人博客

    利用Hexo在GitHub上部署个人博客 1 前言 1 1准备 2 开始 2 1在GitHub上创建仓库 2 2 搭建Hexo并将个人博客部署至Github 2 2 1利用npm安装cnpm 2 2 2 安装并部署Hexo 2 2 3 将个
  • 【解决WARNING报错】WARNING: You are using pip version 21.3.1; however, version 22.0.3 is available.

    比如我在下载PIL模块的时候出现了一下的报错 这个报错是什么原因呢 它其实是提示你你解释器里面的pip 的版本呢已经不能够支持下载该模块了 如果你想下载该模块的话就需要进行pip版本的更新了 那么怎么进行pip版本的更新呢 在WARNING
  • CentOS8网络配置

    CentOS8网络配置 CentOS8已无法正常使用network服务 root localhost systemctl status network Unit network service could not be found root
  • MixFormer学习笔记

    MixFormer End to End Tracking with Iterative Mixed Attention 动机 视觉对象跟踪通常采用特征提取 目标信息集成和边界框估计的多阶段管道 为了简化这一流程 并统一特征提取和目标信息集
  • 关于Qt中的paintEvent事件

    以下均来自与Qt文档 未作任何更改 void QWidget paintEvent QPaintEvent event This event handler can be reimplemented in a subclass to rec
  • js文件引入其他js文件

    function loadJavaScript url success let domScript document createElement script domScript src url success success functi
  • mysql之标识列20

    1 标识列 标识列实际上就是对自动增长概念的讲述 非常简单 只需要理解下面的四个特点即可 标识列 又称为自增长列 含义 可以不用手动的插入值 系统提供默认的序列值 特点 1 标识列必须和主键搭配吗 不一定 但要求是一个key 例如主键 唯一
  • Spark On YARN内存和CPU分配

    软件版本 CDH 5 7 2 JDK 1 7 问题描述 在使用Spark On YARN时 无论是Client模式或者是Cluster模式 当然下面会有这种模式的对比区别 可以添加诸如 plain view plain copy execu
  • 令人头疼的数据输入,认识数据 输入输出

    博客主页 小王又困了 系列专栏 C语言 人之为学 不日近则日退 感谢大家点赞 收藏 评论 目录 一 用scanf函数输入数据 1 printf函数和scanf函数的一般形式 2 格式字符 二 使用scanf函数应该注意的问题 三 字符输入输
  • C++学习(四七七)glsl es使用总结 特点 不同

    1 不支持接口块 Interface Blocks out VS OUT vec2 TexCoords vs out GLSL ES Specification 3 00 pdf中
  • ajax三种错误ie,ie下jquery ajax 80020101错误的解决方法

    注意注释 删除这些注释就可以了 Windows下一个MySQL有些错误的解决方法 1 无论是什么提示 我们有一个直接看错误日志 由于它描述了最具体描述错误日志 于MySQL安装文件夹中找到 my ini简介 看日志保存路径 2 我的错误是
  • Linux——信号及其使用

    信号的基本概念 信号是系统响应某个条件而产生的事件 进程接受到信号会执行相应的操作 软中断信号 用来通知进程发生了异步事件 信号是进程间通信机制中唯一的异步通信机制 一个进程不必通过任何操作来等待信号的到达 系统预先定义好的某些特定事件 信
  • 异构网络的理解

    异构网络 Heterogeneous Network 是一种类型的网络 其是由不同制造商生产的计算机 网络设备 和系统组成的 大部分情况下运行在不同的协议上支持不同的功能或应用 图1 给出了一种异构网络模型 不同类型的网络 通过网关连接到核
  • python 按自己的理解尝试实现fn算法

    贪心算法FN具体步骤如下所述 1 去掉网络中所有的边 网络的每个节点都单独作为一个社区 2 网络中的每个连通部分作为一个社区 将还未加入网络的边分别重新加回网络 如果加入网络的边连接了两个不同的社区 即合并了两个社区 则计算形成的新的社区划
  • 【计算机组成原理笔记】1.2计算机的基本组成

    1 2计算机的基本组成 冯 诺依曼计算机 冯诺依曼计算机的六个特点 冯 诺依曼计算机硬件框图 各部分功能 缺点 计算机硬件框图 以存储器为中心的计算机硬件框图 现代计算机硬件框图 系统复杂性管理方法 计算机的工作步骤 指令格式举例 存储器的

随机推荐

  • Java 核心机制

    Java 核心机制 Java 虚拟机 Java Virtual Machine 简称 JVM 垃圾收集机制 Garbage collection 简称 GC 垃圾收集的目的在移除不再使用的对象 当对象建立的时候垃圾收集期 就开始监控对象的动
  • 图片相相似度计算(Hash、SSIM、compareHist)

    哈希相似度算法 Hash algorithm 用一个快速算法 就达到基本的效果 哈希算法 Hash algorithm 它的作用是对每张图片生成一个固定位数的Hash 值 指纹 fingerprint 字符串 然后比较不同图片的指纹 结果越
  • SDH概述

    1 SDH概述 1 1 SDH产生的技术背景 为什么会产生SDH传输体制 在讲SDH传输体制之前 我们首先要搞清楚SDH到底是什么 那么SDH是什么呢 SDH全称叫做同步数字传输体制 由此可见SDH是一种传输的体制 协议 就像PDH 准同步
  • 基于视频图像的火灾检测算法

    要求 视频帧满足R x y gt gt R x y gt 190 gt 100 140 Y x y Cr x y x y Cb x y Y x y gt 通道均值 Cr x y gt 通道均值 Cb x y lt 通道均值条件且满足利用帧差
  • Java IO流分析整理

    Java中的流 可以从不同的角度进行分类 按照数据流的方向不同可以分为 输入流和输出流 按照处理数据单位不同可以分为 字节流和字符流 按照实现功能不同可以分为 节点流和处理流 输出流 输入流 因此输入和输出都是从程序的角度来说的 字节流 一
  • centos8 stream 屏幕共享 远程桌面 vnc 出错

    在连接vnc之前 干脆直接把防火墙禁用掉 免得出现别的麻烦 systemctl stop firewalld systemctl disable firewalld 然后直接去设置开启屏幕共享 然后用vnc客户端连接 会出现以下错误 Una
  • Redis实现分布式锁的7种方案,及正确使用姿势!

    redis学习笔记 7种方案前言 日常开发中 秒杀下单 抢红包等等业务场景 都需要用到分布式锁 而Redis非常适合作为分布式锁使用 本文将分七个方案展开 跟大家探讨Redis分布式锁的正确使用方式 如果有不正确的地方 欢迎大家指出哈 一起
  • Sqlserver 把小数点后面多余的0去掉

    select convert float 0 05000 结果 0 05 保留有效小数 convert decimal 18 4 0 00900 结果 0 0090 保留有效小数后转换数据类型 convert nvarchar 20 con
  • Python实现房产数据分析与可视化 数据分析 实战

    Python库的选择 话说 工欲善其事 必先利其器 虽然我们已经选择Python来完成剩余的工作 但是我们需要考虑具体选择使用Pytho的哪些利器来帮助我们更快更好地完成剩余的工作 我们可以看一下 在这个任务中 主要涉及到四类工作要完成 c
  • python三个变量互换值_Python中有几种办法交换两个变量的值?

    交换两个变量的值方法 这个面试题如果只写一种当然很简单 没什么可以说的 今天这个面试是问大家有几种办法来实现交换两个变量的值 在没开始看具体答案前 你可以先想想看 下面分别来说说这几种方法 1 方法一 通过新添加中间变量temp的方式 这个
  • Angular脚手架系列:四、使用Angular CLI进行Build (构建) 和 部署

    目录 一 Build 二 ng build 三 Build Targets和Environment 四 Serve 五 部署到nginx 一 Build Build主要会做以下动作 编译项目文件并输出到某个目录 Build targets决
  • ConstrainLayout 基础教程2,近期想跳槽的程序员必看

    特性详解 Visibility behavior 可见性的表现 ConstraintLayout对可见性被标记View GONE的控件 后称 GONE控件 有特殊的处理 一般情况下 GONG控件是不可见的 且不再是布局的一部分 但是在布局计
  • 【服务器】查看服务器文件夹大小

    问题描述 服务器的接口突然访问不到了 报错Networ Error 直接想到了 是不是数据库连不上了导致的 然后看了下服务器的硬盘占用情况 当然了 这里是处理过后的了 发现有问题的那个框框那个Avail为0了 查看文件夹占用 查看某个目录下
  • 旋转数组问题“环状替换”解法最详细的说明

    leecode 旋转数组 问题 环状替换 解法的思路 小白向 思路 假设一数组 a 1 2 3 4 5 6 7 8 9 移动位数k 3 从1开始 要将a 0 右移三位 移到a 3 a 3 右移三位 移到a 6 a 6 移到a 0 回到了a
  • 蓝桥杯2020年第十一届国赛真题-重复字符串

    题目描述 如果一个字符串 S 恰好可以由某个字符串重复 K 次得到 我们就称 S 是 K 次重复字符串 例如 abcabcabc 可以看作是 abc 重复 3 次得到 所以 abcabcabc 是 3 次重复字符串 同理 aaaaaa 既是
  • oracle 中使用 select a into b 时遇到空值问题

    今天一朋友问及我这个问题 当记录不存在 会提示 no data 的错误 下面是网上这类问题的解决方法 转载 当在PL SQL中执行SELECT INTO 语句时 如果返回结果集为空 则回触发NO DATA FOUND错误 但是当 SELEC
  • SSL/TLS原理 详细整理版

    1 SSL TLS握手 简化版 浏览器 服务器 发起 gt 1 浏览器通知服务器浏览器所支持的加密协议 接收 接收 lt 2 服务器通知浏览器从1中选用的加密协议 并给予证书 发起 3 用CA的公钥鉴别服务器的证书是否有效 有效则生成一个随
  • element 表格两级单元格合并功能

    代码如下
  • JavaWeb中servlet到底是干什么的

    JavaWeb中servlet到底是干什么的 javaweb工程包括 src下的 java文件WebRoot下的 jsp js等文件当工程运行时 tomcat先把 jsp gt java gt class 计算机只识别 class文件ser
  • andoid逐帧动画oom_帧动画内存OOM?不存在的!—— SurfaceView逐帧解析

    Android 提供了AnimationDrawable用于实现帧动画 在动画开始之前 所有帧的图片都被解析并占用内存 一旦动画较复杂帧数较多 在低配置手机上容易发生 OOM 即使不发生 OOM 也会对内存造成不小的压力 下面代码展示了一个