解决 Android 逐帧动画Drawable Animation 引起的OOM以及卡顿问题

前言

逐帧动画 (Frame By Frame) 是 Android 系统提供的一种常见的动画形式,通过播放一组连续的图片资源形成动画。当我们想用一组连续的图片播放动画时,首先想到的就是使用系统提供的逐帧动画方式。接下来,我们将简单说明如何使用逐帧动画,以及分析逐帧动画存在的优缺点,最后给出我们的解决方案。

逐帧动画

  • 第一步,将我们所需要的动画素材资源放置在 res/drawable 目录下,切记不要因为是动画所以就错误的将素材资源放置在 res/anim 目录下。
  • 第二步,在 res/anim 目录下新建 drawable 文件 loading.xml ,如下


animation-list 为 drawable 文件的根标签,android:oneshot 设置动画是否只播放一次,子标签 item 具体定义每一帧的动画,android:drawable 定义这一帧动画所使用的资源,android:duration 设置动画的持续时间。

  • 第三步,给想要显示动画的 ImageView 设置资源动画,然后开启动画


我们能看到,逐帧动画使用起来是如此的简单方便,所以当我们想要通过一组图片素材来实现动画的时候首选的就是以上的方案。但是我们却忽略了一个情况,当图片素材很多并且每张图片都很大的情况下,使用以上的方法手机会出现 OOM 以及卡顿问题,这是帧动画的一个比较明显的缺点。

为什么帧动画会出现 OOM 以及卡顿?

我们知道,在第三步给 ImageView 设置图片资源的时候,因为 loading.xml 文件中定义了一系列的图片素材,系统会按照每个定义的顺序把所有的图片都读取到内存中,而系统读取图片的方式是 Bitmap 位图形式,所以就导致了 OOM 的发生。

解决方案

既然一次性读取所有的图片资源会导致内存溢出,那么我们能想到的解决方法就是按照动画的顺序,每次只读取一帧动画资源,读取完毕再显示出来,如果图片过大,我们还需要对图片进行压缩处理。

技术实现

总体思路是这样的,我们在子线程里读取图片资源(包括图片过大,对图片进行处理),读取完毕后通过主线程的 Handler 将在子线程的数据(主要是 Bitmap)发送到主线程中,然后再把 Bitmp 绘制显示出来,每隔一段时间不断读取,然后依次显示出来,这样视觉上就有了动画的效果。实现代码如下


public class AnimationView extends View implements Handler.Callback {

    public static final int DEFAULT_ANIM_TIME = 100;

    public static final int PROCESS_DATA = 1;
    public static final int PROCESS_ANIM_FINISH = 1 << 1;
    public static final int PROCESS_DELAY = 1 << 2;



    public AnimData mCurAnimData;
    public int mCurAnimPos;
    public boolean mIsRepeat;

    public int mAnimTime;

    private Handler mHandler ;
    private ProcessAnimThread mProcessThread;
    private Bitmap mCurShowBmp;

    private List<AnimData> mAnimDataList = new ArrayList<>();

    public AnimationView(Context context) {
        this(context,null);
    }

    public AnimationView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public AnimationView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init(){
        mHandler = new Handler(this);
        mProcessThread = new ProcessAnimThread(getContext(),mHandler);
        mAnimTime = DEFAULT_ANIM_TIME;
    }

    public void setIsRepeat(boolean repeat){
        mIsRepeat = repeat;
    }
    private int mGravity;
    public void SetGravity(int gravity)
    {
        mGravity = gravity;
        invalidate();
    }

    public void setData(List<AnimData> list){
        if (list != null ){
            mAnimDataList.addAll(list);
        }
    }

    private Matrix mTempMatrix = new Matrix();
    @Override
    protected void onDraw(Canvas canvas) {

        if(mCurShowBmp != null && !mCurShowBmp.isRecycled())
        {
            int x = 0;
            int y = 0;
            float scaleX = 1f;
            float scaleY = 1f;
            switch(mGravity & Gravity.HORIZONTAL_GRAVITY_MASK)
            {
                case Gravity.LEFT:
                    x = 0;
                    break;

                case Gravity.RIGHT:
                    x = this.getWidth() - mCurShowBmp.getWidth();
                    break;

                case Gravity.CENTER_HORIZONTAL:
                    x = (this.getWidth() - mCurShowBmp.getWidth()) / 2;
                    break;

                case Gravity.FILL_HORIZONTAL:
                {
                    int w = mCurShowBmp.getWidth();
                    if(w > 0)
                    {
                        scaleX = (float)this.getWidth() / (float)w;
                    }
                    break;
                }

                default:
                    break;
            }
            switch(mGravity & Gravity.VERTICAL_GRAVITY_MASK)
            {
                case Gravity.TOP:
                    y = 0;
                    break;

                case Gravity.BOTTOM:
                    y = this.getHeight() - mCurShowBmp.getHeight();
                    break;

                case Gravity.CENTER_VERTICAL:
                    y = (this.getHeight() - mCurShowBmp.getHeight()) / 2;
                    break;

                case Gravity.FILL_VERTICAL:
                {
                    int h = mCurShowBmp.getHeight();
                    if(h > 0)
                    {
                        scaleY = (float)this.getHeight() / (float)h;
                    }
                    break;
                }

                default:
                    break;
            }
            if(scaleX == 1 && scaleY != 1)
            {
                scaleX = scaleY;
                switch(mGravity & Gravity.HORIZONTAL_GRAVITY_MASK)
                {
                    case Gravity.RIGHT:
                        x = this.getWidth() - (int)(mCurShowBmp.getWidth() * scaleX);
                        break;
                    case Gravity.CENTER_HORIZONTAL:
                        x = (this.getWidth() - (int)(mCurShowBmp.getWidth() * scaleX)) / 2;
                        break;
                }
            }
            else if(scaleX != 1 && scaleY == 1)
            {
                scaleY = scaleX;
                switch(mGravity & Gravity.VERTICAL_GRAVITY_MASK)
                {
                    case Gravity.BOTTOM:
                        y = this.getHeight() - (int)(mCurShowBmp.getHeight() * scaleY);
                        break;
                    case Gravity.CENTER_VERTICAL:
                        y = (this.getHeight() - (int)(mCurShowBmp.getHeight() * scaleY)) / 2;
                        break;
                }
            }
            mTempMatrix.reset();
            mTempMatrix.postScale(scaleX, scaleY);
            mTempMatrix.postTranslate(x, y);
            canvas.drawBitmap(mCurShowBmp, mTempMatrix, null);
        }
    }

    private boolean mHasStarted = false;
    public void start(){

        mHasStarted = true;
        if (mWidth == 0 || mHeight == 0 ){
            return;
        }

        startPlay();

    }

    private void startPlay() {

        if ( mAnimDataList != null && mAnimDataList.size() > 0 ){

            mCurAnimPos = 0;
            AnimData animData = mAnimDataList.get(mCurAnimPos);
            mCurShowBmp = ImageUtil.getBitmap(getContext(),animData.filePath,mWidth,mHeight);
            invalidate();
            if (mListener != null ){
                mListener.onAnimChange(mCurAnimPos,mCurShowBmp);
            }
            checkIsPlayNext();
        }
    }

    private void playNext(final int curAnimPosition ){

        Message msg = Message.obtain();
        msg.what = PROCESS_DELAY;
        msg.arg1 = curAnimPosition;
        mHandler.sendMessageDelayed(msg,mAnimTime);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();

    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        quit();
    }

    private void quit(){

        mHasStarted = false;
        if (mProcessThread != null ){
            mProcessThread.clearAll();
        }
    }

    private int mWidth;
    private int mHeight;
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mWidth = w;
        mHeight = h;
        if (mProcessThread != null ){
            mProcessThread.setSize(w,h);
        }
        if (mHasStarted){
            startPlay();
        }

    }

    private boolean mHavePause = false;
    public void pause(){
        mHavePause = true;
        mHandler.removeMessages(PROCESS_DELAY);
    }

    public void resume(){
        if (mHavePause && mHasStarted){
            checkIsPlayNext();

        }
    }
    @Override
    public boolean handleMessage(Message msg) {

        switch (msg.what){
            case PROCESS_ANIM_FINISH:{

                Bitmap bitmap = (Bitmap) msg.obj;
                if (bitmap != null){
                    if (mCurShowBmp != null ){
                        mCurShowBmp.recycle();
                        mCurShowBmp = null;
                    }
                    mCurShowBmp = bitmap;
                    if (mListener != null ){
                        mListener.onAnimChange(mCurAnimPos,bitmap);
                    }
                    invalidate();

                }
                checkIsPlayNext();
                break;
            }
            case PROCESS_DELAY:{
                int curAnimPosition = msg.arg1;
                AnimData data = mAnimDataList.get(curAnimPosition);
                mProcessThread.processData(data);
                break;
            }
        }
        return true;
    }
    private void checkIsPlayNext() {
        mCurAnimPos ++;
        if ( mCurAnimPos >= mAnimDataList.size() ){
            if (mIsRepeat){
                mCurAnimPos = 0;
                playNext(mCurAnimPos);
            } else {
                if ( mListener != null ){
                    mListener.onAnimEnd();
                }
            }
        } else {
            playNext(mCurAnimPos);
        }
    }
    private AnimCallBack mListener;
    public void setAnimCallBack(AnimCallBack callBack){
        mListener = callBack;
    }

    public interface AnimCallBack{

        void onAnimChange(int position, Bitmap bitmap);
        void onAnimEnd();
    }

    public static class AnimData{
         public Object filePath;

    }
    public static class ProcessAnimThread{

        private HandlerThread mHandlerThread;
        private Handler mProcessHandler;
        private Handler mUiHandler;

        private AnimData mCurAnimData;

        private int mWidth;
        private int mHeight;
        private WeakReference<Context> mContext;

        public ProcessAnimThread(Context context, Handler handler){
            mUiHandler = handler;
            mContext = new WeakReference<Context>(context);
            init();
        }

        public void setSize(int width,int height){
            mWidth = width;
            mHeight = height;
        }

        private void init(){

            mHandlerThread = new HandlerThread("process_anim_thread");
            mHandlerThread.start();

            mProcessHandler = new Handler(mHandlerThread.getLooper(), new Handler.Callback() {
                @Override
                public boolean handleMessage(Message msg) {
                    // 消息是在子线程 HandlerThread 里面被处理,所以这里的 handleMessage 在
                    //子线程里被调用
                    switch (msg.what){
                        case PROCESS_DATA:{
                            AnimData animData = (AnimData) msg.obj;
                            Bitmap bitmap = ImageUtil.getBitmap(mContext.get(),animData.filePath,mWidth,mHeight);
                            if (bitmap != null ){
                                Message finishMsg = Message.obtain();
                                finishMsg.what = PROCESS_ANIM_FINISH;
                                finishMsg.obj = bitmap;
                                //消息处理完毕,使用主线程的 Handler 将消息发送到主线程
                                mUiHandler.sendMessage(finishMsg);
                            }
                            break;
                        }
                    }
                    return true;
                }
            });

        }

        public void processData(AnimData animData){

            if ( animData != null ){
                Message msg = Message.obtain();
                msg.what = PROCESS_DATA;
                msg.obj = animData;
                mProcessHandler.sendMessage(msg);
            }

        }
        public void clearAll(){

            mHandlerThread.quit();
            mHandlerThread = null;
        }
    }
}

  • 首先定义静态的内部类 AnimData,作为我们的动画实体类,filePath 为动画的路径,可以是 res 资源目录下,也可以是 外部存储的路径。


  • 接下来定义封装 ProcessAnimThread 类,用以将资源图片读取为 Bitmap,如果图片过大,我们还需要将其压缩处理。ProcessAnimThread 类中,最为关键的是 HandlerThread ,这是自带有 Looper 的 Thread,继承自 Thread。前面我们说过在子线程里读取 Bitmap, HandlerThread 就是我们上面提及的子线程,使用方法上,我们先构造 HandlerThread ,然后调用 start() 方法开启线程,这时候 HandlerThread 里的 Looper 已经启动可以出来消息了,最后通过这个 Looper 构造 Handler(例子中为 mProcessHandler 变量)。完成以上步骤之后,我们通过 mProcessHandler 发送的消息最终会在 子线程里被处理,处理完毕之后,再讲结果发送到主线程


  • 接下来看主线程收到消息后如何处理。首先将结果取出来,然后刷新显示,接着判断队列是否以及处理结束,未结束则通过发送延迟的消息继续读取图片。


AnimationView 使用步骤

  • 构造帧动画数据队列


  • 调用 AnimationView 的 start() 方法开启动画

写在最后

AnimationView 是解决方案里的一个简单实现,由于知识水平有限,难免有错误和遗漏,欢迎指正。
最后,附上项目地址 https://github.com/hanilala/CoolCode

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,366评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,521评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,689评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,925评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,942评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,727评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,447评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,349评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,820评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,990评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,127评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,812评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,471评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,017评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,142评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,388评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,066评论 2 355

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,183评论 25 707
  • java 接口的意义-百度 规范、扩展、回调 抽象类的意义-乐视 为其子类提供一个公共的类型封装子类中得重复内容定...
    交流电1582阅读 2,233评论 0 11
  • 前几天整理了Java面试题集合,今天再来整理下Android相关的面试题集合.如果你希望能得到最新的消息,可以关注...
    Boyko阅读 3,636评论 8 135
  • 整本书用平实易懂的文字将现实之下那些赤裸裸的腐败黑暗全部一览无余的展现出来,读的让人喘不过气来,甚至不忍继续看下...
    清欢随喜阅读 1,151评论 2 2
  • 近些了,近些了。都说近乡情怯。扑簌簌两行热泪。手抖抖一抔黄土。远游的孩子啊,回来了!从小便跟随着父亲背井离...
    你的样子1314阅读 797评论 1 51