Android帧动画分析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/binbinqq86/article/details/78127284

转载请注明出处:http://blog.csdn.net/binbinqq86/article/details/78127284

说起动画,相信大家都不陌生,每个Android开发者都会接触到。Android中的动画大致可以分为传统动画和属性动画:

  1. 传统动画
    a. 帧动画(FrameAnimation)
    b. 补间动画(TweenAnimatioin)
    1. alpha(淡入淡出)
    2. translate(平移)
    3. scale(缩放)
    4. rotate(旋转)
  2. 属性动画

今天我们主要来分析一下其中的一个分支:帧动画。帧动画一般是多张连续的图片进行连贯的顺序播放,从而在视觉上产生一种动画的效果,最简单的实现方式就是采用系统提供好的方法来实现:

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个阶段

具体含义大家可以去官网查阅,这里就不再细说。最后给出一个之前公司做直播时的大礼物动画效果:

这里写图片描述

同样最后给出源码下载地址,有疑问的朋友可以在下面留言,谢谢大家!

源码下载

猜你喜欢

转载自blog.csdn.net/binbinqq86/article/details/78127284