Android帧动画分析
来源:互联网 发布:iapp制作文字游戏源码 编辑:程序博客网 时间:2024/06/03 22:42
转载请注明出处:http://blog.csdn.net/binbinqq86/article/details/78127284
说起动画,相信大家都不陌生,每个Android开发者都会接触到。Android中的动画大致可以分为传统动画和属性动画:
- 传统动画
a. 帧动画(FrameAnimation)
b. 补间动画(TweenAnimatioin)- alpha(淡入淡出)
- translate(平移)
- scale(缩放)
- rotate(旋转)
- 属性动画
今天我们主要来分析一下其中的一个分支:帧动画。帧动画一般是多张连续的图片进行连贯的顺序播放,从而在视觉上产生一种动画的效果,最简单的实现方式就是采用系统提供好的方法来实现:
iv.setBackgroundResource(R.drawable.values);AnimationDrawable anim = (AnimationDrawable) iv.getBackground();anim.start();
values为动画资源文件:(这里只放了前10张)
<?xml version="1.0" encoding="utf-8"?><animation-list xmlns:android="http://schemas.android.com/apk/res/android" android:oneshot="false"> <item android:drawable="@mipmap/yacht1" android:duration="150" /> <item android:drawable="@mipmap/yacht2" android:duration="150" /> <item android:drawable="@mipmap/yacht3" android:duration="150" /> <item android:drawable="@mipmap/yacht4" android:duration="150" /> <item android:drawable="@mipmap/yacht5" android:duration="150" /> <item android:drawable="@mipmap/yacht6" android:duration="150" /> <item android:drawable="@mipmap/yacht7" android:duration="150" /> <item android:drawable="@mipmap/yacht8" android:duration="150" /> <item android:drawable="@mipmap/yacht9" android:duration="150" /> <item android:drawable="@mipmap/yacht10" android:duration="150" /></animation-list>
里面的oneshot属性代表是否是重复播放。这里我们采用的图片是1080*1920分辨率的,显示在1080p的手机上并铺满屏幕,来看下显示效果:
来看一下内存的使用情况:
可以看到,内存直接暴涨到来94M,这是为什么呢,假如我们把所有的几十张图片全部放出来呢,运行一下,结果程序直接崩掉了,由此我们可以猜想系统是一次性加载了所有的资源到内存中去的,这样的话,这种方式就只能做一些简单的小动画了。为了验证我们的猜想,下面继续一探究竟。
首先来分析下图片在内存中的占用情况:
ARGB_8888:A->8bit->一个字节,R->8bit->一个字节,
G->8bit->一个字节,B->8bit->一个字节,即8888,
一个像素总共占四个字节,8+8+8+8=32bit=4byte
ARGB_4444:A->4bit->半个字节,R->4bit->半个字节,
G->4bit->半个字节,B->4bit->半个字节,即4444,
一个像素总共占两个字节,4+4+4+4=16bit=2byte
RGB_565:R->5bit->半个字节,G->6bit->半个字节,
B->5bit->半个字节,即565,一个像素总共占两个字节,
5+6+5=16bit=2byte
ALPHA_8:A->8bit->一个字节,即8,一个像素总共占一个字节,
8=8bit=1byte
图片在内存中一般会有这几种存在方式,默认为ARGB_8888,这样我们一张1080*1920图片放在xxdpi下展示在1080*1920手机上铺满屏幕所占内存:
1080*1920*4/1024/1024=7.91M
所以可以得出,系统原生动画加载上述10张图内存为94M的缘故:系统把所有图片一次性加载到内存中了。
注意:如果放在xdpi下,则图片呈现在1080*1920手机上会被放
大1.5倍,这时所占内存也增加了1.5倍,
开发的时候需要注意哦!!!
为了进一步验证我们的猜想,下面去源码里面扒一扒!首先来看下getBackground这个方法,它返回一个Drawable,我们去看Drawable里面到底是怎么把资源文件加载为动画的:
/** * Create a drawable from an XML document. For more information on how to * create resources in XML, see * <a href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>. */ public static Drawable createFromXml(Resources r, XmlPullParser parser) throws XmlPullParserException, IOException { return createFromXml(r, parser, null); }
继续看createFromXml这个方法:
/** * Create a drawable from an XML document using an optional {@link Theme}. * For more information on how to create resources in XML, see * <a href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>. */ public static Drawable createFromXml(Resources r, XmlPullParser parser, Theme theme) throws XmlPullParserException, IOException { AttributeSet attrs = Xml.asAttributeSet(parser); int type; //noinspection StatementWithEmptyBody while ((type=parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { // Empty loop. } if (type != XmlPullParser.START_TAG) { throw new XmlPullParserException("No start tag found"); } Drawable drawable = createFromXmlInner(r, parser, attrs, theme); if (drawable == null) { throw new RuntimeException("Unknown initial tag: " + parser.getName()); } return drawable; }
/** * Create a drawable from inside an XML document using an optional * {@link Theme}. Called on a parser positioned at a tag in an XML * document, tries to create a Drawable from that tag. Returns {@code null} * if the tag is not a valid drawable. */ public static Drawable createFromXmlInner(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) throws XmlPullParserException, IOException { return r.getDrawableInflater().inflateFromXml(parser.getName(), parser, attrs, theme); }
这里又引出了DrawableInflater这个类,调用了它的inflateFromXml方法,跟进去看:
/** * Inflates a drawable from inside an XML document using an optional * {@link Theme}. * <p> * This method should be called on a parser positioned at a tag in an XML * document defining a drawable resource. It will attempt to create a * Drawable from the tag at the current position. * * @param name the name of the tag at the current position * @param parser an XML parser positioned at the drawable tag * @param attrs an attribute set that wraps the parser * @param theme the theme against which the drawable should be inflated, or * {@code null} to not inflate against a theme * @return a drawable * * @throws XmlPullParserException * @throws IOException */ @NonNull public Drawable inflateFromXml(@NonNull String name, @NonNull XmlPullParser parser, @NonNull AttributeSet attrs, @Nullable Theme theme) throws XmlPullParserException, IOException { // Inner classes must be referenced as Outer$Inner, but XML tag names // can't contain $, so the <drawable> tag allows developers to specify // the class in an attribute. We'll still run it through inflateFromTag // to stay consistent with how LayoutInflater works. if (name.equals("drawable")) { name = attrs.getAttributeValue(null, "class"); if (name == null) { throw new InflateException("<drawable> tag must specify class attribute"); } } Drawable drawable = inflateFromTag(name); if (drawable == null) { drawable = inflateFromClass(name); } drawable.inflate(mRes, parser, attrs, theme); return drawable; }
最终调用的是inflateFromTag方法:
private Drawable inflateFromTag(@NonNull String name) { switch (name) { case "selector": return new StateListDrawable(); case "animated-selector": return new AnimatedStateListDrawable(); case "level-list": return new LevelListDrawable(); case "layer-list": return new LayerDrawable(); case "transition": return new TransitionDrawable(); case "ripple": return new RippleDrawable(); case "color": return new ColorDrawable(); case "shape": return new GradientDrawable(); case "vector": return new VectorDrawable(); case "animated-vector": return new AnimatedVectorDrawable(); case "scale": return new ScaleDrawable(); case "clip": return new ClipDrawable(); case "rotate": return new RotateDrawable(); case "animated-rotate": return new AnimatedRotateDrawable(); case "animation-list": return new AnimationDrawable(); case "inset": return new InsetDrawable(); case "bitmap": return new BitmapDrawable(); case "nine-patch": return new NinePatchDrawable(); default: return null; } }
inflateFromTag方法中,根据不同的标签名称去生成不同的drawable,回到我们的场景对应的就是AnimationDrawable。我们再去看看这个类,其中有一个方法:
private void inflateChildElements(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) throws XmlPullParserException, IOException { int type; final int innerDepth = parser.getDepth()+1; int depth; while ((type=parser.next()) != XmlPullParser.END_DOCUMENT && ((depth = parser.getDepth()) >= innerDepth || type != XmlPullParser.END_TAG)) { if (type != XmlPullParser.START_TAG) { continue; } if (depth > innerDepth || !parser.getName().equals("item")) { continue; } final TypedArray a = obtainAttributes(r, theme, attrs, R.styleable.AnimationDrawableItem); final int duration = a.getInt(R.styleable.AnimationDrawableItem_duration, -1); if (duration < 0) { throw new XmlPullParserException(parser.getPositionDescription() + ": <item> tag requires a 'duration' attribute"); } Drawable dr = a.getDrawable(R.styleable.AnimationDrawableItem_drawable); a.recycle(); if (dr == null) { while ((type=parser.next()) == XmlPullParser.TEXT) { // Empty } if (type != XmlPullParser.START_TAG) { throw new XmlPullParserException(parser.getPositionDescription() + ": <item> tag requires a 'drawable' attribute or child tag" + " defining a drawable"); } dr = Drawable.createFromXmlInner(r, parser, attrs, theme); } mAnimationState.addFrame(dr, duration); if (dr != null) { dr.setCallback(this); } } }
看第42行,有一个AnimationState类,addFrame方法就是把动画关键帧加入进来,具体加入哪里了。我们跟踪可以看到加入它的父类里面的mDrawables这个变量了,它是一个数组,这一下就全都明白了,果然是一次性加入所以的图片到内存中去了。。。
通过以上分析也就解释了为什么图片过多的时候程序就直接crash了。所以这种方案只能用来做一些很小的帧动画。那么有没有什么方案能播放大量图片同时又不占用内存呢???当然有,我们可以想到的就是逐帧播放,及时回收无用的图片,这样内存就不会导致暴涨。看代码:
public void animateFrameDrawableResourceOneByOne(final int resIds[], final int durations[], final ImageView imageView,final int frameNumber, final OnAnimationListener onAnimationListener){ imageView.setImageResource(resIds[frameNumber]); handler.postDelayed(new Runnable() { @Override public void run() { if(frameNumber<resIds.length-1){ animateFrameDrawableResourceOneByOne(resIds,durations,imageView,frameNumber+1,onAnimationListener); }else{ if (onAnimationListener != null) { onAnimationListener.onAnimationEnd(); } } } },durations[frameNumber]); }
通过代码我们可以看出,每隔规定的帧率,去加载下一张图片,来看看内存情况:(同样一次性加载33张全部高清无码大图)
可以看到内存确实是维持在一个稳定的范围内了,基本上是两张图片所占的内存16M加上APP原始内存16M,峰值在45M,但是我们又发现内存抖动厉害,锯齿一样的不停创建和释放,这就导致cpu消耗大量资源去做这些事情,很明显不是我们想要的结果,那么怎么处理这种抖动呢?这里我们就要用到BitmapFactory.Options的一个属性了—inBitmap,我们来看下官方的注释(装b时刻):
/** * If set, decode methods that take the Options object will attempt to * reuse this bitmap when loading content. If the decode operation * cannot use this bitmap, the decode method will return * <code>null</code> and will throw an IllegalArgumentException. The * current implementation necessitates that the reused bitmap be * mutable, and the resulting reused bitmap will continue to remain * mutable even when decoding a resource which would normally result in * an immutable bitmap.</p> * * <p>You should still always use the returned Bitmap of the decode * method and not assume that reusing the bitmap worked, due to the * constraints outlined above and failure situations that can occur. * Checking whether the return value matches the value of the inBitmap * set in the Options structure will indicate if the bitmap was reused, * but in all cases you should use the Bitmap returned by the decoding * function to ensure that you are using the bitmap that was used as the * decode destination.</p> * * <h3>Usage with BitmapFactory</h3> * * <p>As of {@link android.os.Build.VERSION_CODES#KITKAT}, any * mutable bitmap can be reused by {@link BitmapFactory} to decode any * other bitmaps as long as the resulting {@link Bitmap#getByteCount() * byte count} of the decoded bitmap is less than or equal to the {@link * Bitmap#getAllocationByteCount() allocated byte count} of the reused * bitmap. This can be because the intrinsic size is smaller, or its * size post scaling (for density / sample size) is smaller.</p> * * <p class="note">Prior to {@link android.os.Build.VERSION_CODES#KITKAT} * additional constraints apply: The image being decoded (whether as a * resource or as a stream) must be in jpeg or png format. Only equal * sized bitmaps are supported, with {@link #inSampleSize} set to 1. * Additionally, the {@link android.graphics.Bitmap.Config * configuration} of the reused bitmap will override the setting of * {@link #inPreferredConfig}, if set.</p> * * <h3>Usage with BitmapRegionDecoder</h3> * * <p>BitmapRegionDecoder will draw its requested content into the Bitmap * provided, clipping if the output content size (post scaling) is larger * than the provided Bitmap. The provided Bitmap's width, height, and * {@link Bitmap.Config} will not be changed. * * <p class="note">BitmapRegionDecoder support for {@link #inBitmap} was * introduced in {@link android.os.Build.VERSION_CODES#JELLY_BEAN}. All * formats supported by BitmapRegionDecoder support Bitmap reuse via * {@link #inBitmap}.</p> * * @see Bitmap#reconfigure(int,int, android.graphics.Bitmap.Config) */ public Bitmap inBitmap;
基本的意思呢就是如果采用了这个属性,系统就会重用内存,当加载新的图片时不需要再去开辟新内存了,这样一想,果然是可以解决抖动的问题哦,当然这么使用是有一定条件的,下面结合一张图你就看的更明白了:
使用inBitmap后:
一切应该都很明了了。。。下面说一下使用条件:
根据注释相信你也能看明白,在4.4之后,只需要新的图片不大于原来的图片即可,而在4.4之前就比较严格了,必须要求两张图片的宽高相等,并且inSampleSize=1
而官方也给出了一个方法来判断是否可以重用:
public static boolean canUseForInBitmap( Bitmap candidate, BitmapFactory.Options targetOptions) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { // From Android 4.4 (KitKat) onward we can re-use if the byte size of // the new bitmap is smaller than the reusable bitmap candidate // allocation byte count. int width = targetOptions.outWidth / targetOptions.inSampleSize; int height = targetOptions.outHeight / targetOptions.inSampleSize; int byteCount = width * height * getBytesPerPixel(candidate.getConfig()); return byteCount <= candidate.getAllocationByteCount(); } // On earlier versions, the dimensions must match exactly and the inSampleSize must be 1 return candidate.getWidth() == targetOptions.outWidth && candidate.getHeight() == targetOptions.outHeight && targetOptions.inSampleSize == 1; }
大家也可以移步这里去观看更详细的介绍:
https://developer.android.com/topic/performance/graphics/index.html
来看我们新的实现方案:
private void animateDrawableManually(final BitmapFactory.Options options, final int resIds[], final int durations[], final List<MyFrame> myFrame, final ImageView imageView, final OnAnimationListener onAnimationListener, final int frameNumber) { MyFrame thisFrame = null; if (frameNumber == 0) { thisFrame = new MyFrame(); thisFrame.duration = durations[0]; thisFrame.bitmap = BitmapFactory.decodeResource(imageView.getContext().getApplicationContext().getResources(), resIds[0], options); myFrame.add(thisFrame); } else { thisFrame = myFrame.get(1); myFrame.remove(0); } options.inMutable = true;//true 这样返回的bitmap 才是mutable 也就是可重用的,否则是不能重用的 options.inSampleSize=1; options.inBitmap = thisFrame.bitmap; imageView.setImageBitmap(thisFrame.bitmap); handler.postDelayed(new Runnable() { @Override public void run() { if (frameNumber < resIds.length - 1) { //准备并播放下一帧 MyFrame nextFrame = new MyFrame(); nextFrame.duration = durations[frameNumber + 1]; nextFrame.bitmap = BitmapFactory.decodeResource(imageView.getContext().getApplicationContext().getResources(), resIds[frameNumber + 1], options); boolean can1 = Utils.canUseForInBitmap(nextFrame.bitmap, options); Log.e(TAG, "run: " + "$$$" + can1+"$"+nextFrame.bitmap.getHeight()); myFrame.add(nextFrame); animateDrawableManually(options, resIds, durations, myFrame, imageView, onAnimationListener, frameNumber + 1); } else { options.inBitmap.recycle(); myFrame.clear(); if (onAnimationListener != null) { onAnimationListener.onAnimationEnd(); } } } }, thisFrame.duration); }
看一下现在的内存情况:
可以看到,内存的锯齿已经消失了。而且对cpu的耗用情况也减少很多(我们的动画时间是5秒,这个范围内基本没有波动)
那么下面再来看另外一种方式:GIF
这种方式呢,一般可以采用第三方库来实现,比较出名的有GifImageView,还有一种方式是系统自带的Movie类来解析,具体可以参考郭神的文章:
http://blog.csdn.net/guolin_blog/article/details/11100315
看一下这种方式对内存和cpu的影响:
可以看到这种方式对内存不怎么消耗,但是对cpu的消耗却是比较高的,这样对整体性能依然是不好的,这种解析gif的原理基本上都是通过底层native来解析图片关键帧,可想而知对cpu的压力还是挺大的。(需关闭硬件加速才有效果,所以对gpu无影响,压力都在cpu)
剩下的两种方式就是采用surfaceView和GLSurfaceView,前者在一般视频类应用中用的比较多,而后者一般用在游戏中,来看下这两者的效果:
surfaceView:
GLSurfaceView:
对比分析可以发现,surfaceView对cpu的压力还是挺大的,而glSurfaceView基本可以接受,两者对内存的占用率还是挺好的。
以上就是几种方案的对比分析,综合起来可以从以下几点来具体场景具体使用:
1、兼容性
2、耗电量
3、绘制速度
openGL虽然速度快,但是对机器的兼容性不好,而耗电量的话就要从cpu和gpu的占用率来分析了,越高越耗电,你的应用越不流畅,性能越卡,一般在使用过程中重用内存来给imageView设置图片就可以满足需求,特殊情况则可以特殊处理。
GPU选项的开启可以从这里设置:
开启后就可以监视gpu的性能了。里面那根绿色水平线代表16ms,要确保一秒内打到60fps,你需要确保这些帧的每一条线都在绿色的16ms标记线之下。任何时候你看到一个竖线超过了绿色的标记现,你就会看到你的动画有卡顿现象产生。具体右边的几个色块呢,在不同Android版本都有差异:
在4.x的系统中,只分了3个阶段,而在5.x系统中细分成4个阶段,而在6.0系统中更进一步细分为了9个阶段
具体含义大家可以去官网查阅,这里就不再细说。最后给出一个之前公司做直播时的大礼物动画效果:
同样最后给出源码下载地址,有疑问的朋友可以在下面留言,谢谢大家!
源码下载
- Android帧动画分析
- Android 动画分析之Tween动画分析
- Android 动画分析之Tween动画分析
- android 动画模块 分析
- 分析android动画模块
- 分析android动画模块
- 分析android动画模块
- 分析android动画模块
- 分析android动画模块
- android 动画模块分析
- android 动画分析
- 分析android动画模块
- 分析android动画模块
- android 动画分析
- android 动画模块 分析
- 分析android动画模块
- 分析android动画模块
- 分析android动画模块
- 代码设置textsize(不用diptopx,pxtodip啦)
- hdu 2795 Billboard
- 移除两个数组中相同的值
- # Software-eng lab 3
- shell批量对比不同host的目录文件
- Android帧动画分析
- 如何使用SVN下载github源文件
- 日常生活用到的小命令liunx
- bzoj 3613: [Heoi2014]南园满地堆轻絮 二分答案+贪心
- 数据结构之栈堆和队列
- HDU 5979 Convex (几何)
- 洛谷 2676 [NOIP2015] 子串 DP
- redis主从复制 和一些数据恢复
- [INSTALL_FAILED_DUPLICATE_PERMISSION perm=quicksdk_packageName.permission.JPUSH_MESSAGE pkg=com.shou