Android 帧动画 AnimationDrawable 导致的 OOM ANR 的解决方案

FrameAnimation

如果有播放超多帧动画的需求,直接点击 FrameAnimation 在github查看,百分之99.99能满足你的需求,基本此文就可以终结了。

PS. 此文年久失修,上述代码的具体实现本文已有较大差距,不过整体思路还是可以参考下的。

关于Android帧动画

       当在应用中需要使用帧动画的时候,最先想到的就是Android提供的AnimationDrawable了,但是如果帧动画中如果包含上百帧图片,此时再用AnimationDrawable就不是那么理想了。AnimationDrawable使用一个Drawable数组来存储每一帧的图像,会直接把全部图片加载进内存。随着帧数量的增多,就算性能再强劲的机器也会卡顿、OOM。

使用SurfaceView来实现帧动画的效果

    最近的项目中需要用到大量的帧动画(各种闪瞎24K钛合金狗眼的礼物效果,多的高达200帧),既然AnimationDrawable不行,就想到了两种解决方法。

第一个想到的解决办法就是用openGL来绘制了。

   因为是直播的项目,包含人脸贴图等都是用opengl绘制的,如果用OpenGL绘制一层Texture直接推流还省事。只在主播端处理就行了,但是IOS那边都弄得差不多了,直接原生的不用处理也不会有什么异常什么的。。很尴尬。

第二个就是使用Android自带的surfaceView了

    好吧,第一个不行那就想到Android自带的surfaceView啦。我首先用不同的手机测试了下应用从本地decode一个bitmap的时间(png格式,414*736,大小在30-100k之间),因为帧动画的每帧不会太大,在性能好点的设备上基本保持在10-30ms之间(不推流基本上推流状态下10ms左右,推流状态下20左右),在性能稍差的设备中基本上也不会超过50ms,所以说是没什么大问题的。

在移动设备播放的帧动画一定要尽最大可能的压缩,推荐一个网站,会把图片颜色深度压缩成8位的。TinyPNG

实现思路

整个思路大概如图。Android 帧动画 AnimationDrawable 导致的 OOM ANR 的解决方案_第1张图片
既然不能完全加载到内存,想到的就是类似视频播放或者视频直播类似的思路。首先定义一个Bitmap的缓冲区,边绘制边加载。首先加载一定数量的帧到Bitmap缓冲区,加载完成后通知SurfaceView开始绘制。SurfaceView绘制一帧完成后通知Bitmap缓冲区加载下一帧,同时将绘制过的一帧的从Bitmap缓冲区移除。一帧绘制完成后,绘制线程根据设置的帧间隔休眠一段时间,休眠完成后开始从Bitmap缓冲区获取下一帧,依此类推,一直循环,直到播放完成或者手动停止。按照这种方式实现起来,发现oom卡顿什么的果然不存在了,内存的使用情况如图。
Android 帧动画 AnimationDrawable 导致的 OOM ANR 的解决方案_第2张图片
但是看着这个垃圾桶一个挨一个,这个内存回收情况完全不正常!GC太频繁了。想着应该是这里出现了问题。Android 帧动画 AnimationDrawable 导致的 OOM ANR 的解决方案_第3张图片
频繁的添加移除bitmap,导致了不算太严重的内存抖动。之所以称之为不算太严重,因为大概400ms一次,一次gc花费2ms左右。不看内存,只看运行效果。真的感觉不出来。但是呢,这样显然也是不行滴。
####内存抖动的解决
最常见的解决方法就是对象的复用,创建各种pool。Android也提供了Bitmap的复用方式,在加载bitmap的时候传入一个inBitmap,那么加载的bitmap就会复用原bitmap的内存空间,所以理论上将要复用的bitmap和新加载的bitmap在颜色深度一样的情况下,复用的bitmap宽高要大于新加载的bitmap。50L的桶毕竟最多只能装50L的水。关于inBitmap更多资料可以参考这里,还有这里。(请自备梯子)。 使用起来很简单,大概就是这样

Bitmap mInBitmap;
BitmapFactory.Options mOptions = new BitmapFactory.Options();
mOptions.inMutable = true;
mOptions.inSampleSize = 1;
//mInBitmap不能为null,此处省去赋值
mOptions.inBitmap = mInBitmap;
Bitmap bitmap = BitmapFactory.decodeStream(mAssetManager.open(path), null, mOptions);

然后实现思路就是在这里修改了,把将要删除的哪一帧留下来作为inBitmap。最后再看下内存的使用情况,首先是运行动画前。
Android 帧动画 AnimationDrawable 导致的 OOM ANR 的解决方案_第4张图片
然后是运行动画时
Android 帧动画 AnimationDrawable 导致的 OOM ANR 的解决方案_第5张图片
内存的使用非常平稳,其实一直是加载到内存中的那几帧,和上图抖动的垃圾桶形成了鲜明的对比。内存占用和播放动画之前只多了那么一丢丢。就是你有1000000000张帧动画要播放,还是这么一丢丢。

使用

关于代码我觉得不贴了,贴了也不一定有人看,这里只分享下核心的实现思路。大家有兴趣的可以自己搞一下。更可以方便的直接去github查看和使用SilkyAnimation。可以超级方便的播放帧动画。

SilkyAnimation mAnimation=
                new SilkyAnimation.Builder(mSurfaceView)
                .build();
//初始化完成之后直接就调用start传入file或者asset资源目录播放了
File file=new FIle(Environment.getExternalStorageDirectory() + File.separator+ "bird")
mAnimation.start(file);
//或者
String assetsPath="bird/crow";
mAnimation.start(assetsPath);
//然后你还可有更多设置
new SilkyAnimation.Builder(mSurfaceView)
                //设置常驻内存的缓存数量, 默认5. 
                .setCacheCount(8)
                //设置帧间隔, 默认100
                .setFrameInterval(80)
                //设置缩放类型, 默认fit center,与ImageView的缩放模式通用
                .setScaleType(SilkyAnimation.SCALE_TYPE_FIT_END)
                //设置动画开始结束状态监听
                .setAnimationListener(listener)
                //设置是否支持bitmap复用,默认为true
                .setSupportInBitmap(false)
                //设置循环模式, 默认不循环
                .setRepeatMode(SilkyAnimation.MODE_INFINITE)
                .build();

看上去是不是超级方便,如果有任何问题的话也可以直接在github提交issue。

问题

关于从本地加载图片的方式,当时在想用多线程异步加载或者单线程同步阻塞加载的哪一个。最后选择了单线程同步阻塞加载,因为个人觉得决定加载速度的更多应该是io速度,多线程并不能解决。如果再加上各种锁,或许多线程异步加载并没有什么优势,并且实现起来单线程明显工作量小很多,而且最后实现起来并没有发现因为加载速度导致的问题。预先加载5个到内存,更多是为了对冲加载某个图片耗时异常的风险,如果加载每个超大图片的时间都很长,那么解决的方式只能是增大帧间隔。这就像class4 的tf卡不能用来拍摄4K视频似的。以上也只是个人的想法,如果有错误的地方也欢迎小伙伴们指正。

你可能感兴趣的:(Android)