转载请注明出处: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个阶段
具体含义大家可以去官网查阅,这里就不再细说。最后给出一个之前公司做直播时的大礼物动画效果:
同样最后给出源码下载地址,有疑问的朋友可以在下面留言,谢谢大家!