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
实现思路
整个思路大概如图。
既然不能完全加载到内存,想到的就是类似视频播放或者视频直播类似的思路。首先定义一个Bitmap的缓冲区,边绘制边加载。首先加载一定数量的帧到Bitmap缓冲区,加载完成后通知SurfaceView开始绘制。SurfaceView绘制一帧完成后通知Bitmap缓冲区加载下一帧,同时将绘制过的一帧的从Bitmap缓冲区移除。一帧绘制完成后,绘制线程根据设置的帧间隔休眠一段时间,休眠完成后开始从Bitmap缓冲区获取下一帧,依此类推,一直循环,直到播放完成或者手动停止。按照这种方式实现起来,发现oom卡顿什么的果然不存在了,内存的使用情况如图。
但是看着这个垃圾桶一个挨一个,这个内存回收情况完全不正常!GC太频繁了。想着应该是这里出现了问题。
频繁的添加移除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。最后再看下内存的使用情况,首先是运行动画前。
然后是运行动画时
内存的使用非常平稳,其实一直是加载到内存中的那几帧,和上图抖动的垃圾桶形成了鲜明的对比。内存占用和播放动画之前只多了那么一丢丢。就是你有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(十二)、Android中的图形和动画之属性动画
- Android之 RecyclerView,CardView 详解和相对应的上拉刷新下拉加
- 2014-11-8Android学习------Android 实现图片的旋转--------动画
- android activity切换动画库SwitchLayout
- Android5.0之后 VectorDrawable实现超炫酷动画效果
- Android 动画实现
- android中使用线程池和临时缓存优化网络图片加载