拒绝OOM,打造自定义帧动画

android开发:拒绝OOM,打造自定义帧动画加载框架

转载请标明出处:漆可的博客http://blog.csdn.net/q649381130/article/details/49407129

一、概述

在安卓开发中,帧动画是通过逐帧显示配置在动画资源文件中的图片来实现。然而,这存在一个巨大的风险,由于安卓是一次性把动画资源文件中的所有图片资源全部加载,这就意味着如果图片数目过多,极其容易造成内存溢出。

那么,如何避免这种情况的发生呢。下面我们推出本文的主角SurfaceView。

二、SurfaceView详解

SurfaceView是一个专门用户绘制的view的子类,由于内部已做处理,并且实现了双缓冲,它可以在非UI线程中进行UI操作(谷歌仅为我们提供俩个有该特异功能的控件,另外一个是ProgressBar)。这样可以避免画图任务繁重的时候造成主线程阻塞,从而提高了程序的反应速度。广泛用于游戏开发中的绘制。

SurfaceView的使用非常简单,一般与SurfaceHolder结合使用:

  1. 创建一个集成了SurfaceView的类,并实现SurfaceHolder.Callback实现后SurfaceHolder.Callback必须实现了三个方法,意思也很明白,分别是:

    surfaceCreated:
    surfaceChanged:
    surfaceDestroyed:

  2. 通过getHolder()获取该类的SurfaceHolder;
  3. 调用SurfaceHolder的addCallback(this)方法添加回调;
  4. SurfaceHolder.lockCanvas()获得Canvas对象并锁定画布,该Cavas已经被自动加了同步锁;
  5. 利用获取Canvas的绘图方法进行绘制,本文我们是绘制bitmap;
  6. SurfaceHolder.unlockCanvasAndPost(Canvas canvas)结束锁定画图,并提交改变,将图形显示。

本文就是通过SurfaceView内部开启线程动态的绘制图片。

三、普通方法实现帧动画

先介绍下安卓原生的帧动画使用方法,废话少说,直接上代码,完整源码在文章最后提供下载地址。

在res目录下新建anim文件,该文件下创建frame_demo.xml文件:

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="true" >

<item android:drawable="@drawable/penguin_happy_3_1"   android:duration="150"/>
<item android:drawable="@drawable/penguin_happy_3_2"   android:duration="150"/>
<item android:drawable="@drawable/penguin_happy_3_3"   android:duration="150"/>
<item android:drawable="@drawable/penguin_happy_3_4"   android:duration="150"/>
<item android:drawable="@drawable/penguin_happy_3_5"   android:duration="150"/>
<item android:drawable="@drawable/penguin_happy_3_6"   android:duration="150"/>
<item android:drawable="@drawable/penguin_happy_3_7"   android:duration="150"/>
<item android:drawable="@drawable/penguin_happy_3_8"   android:duration="150"/>
<item android:drawable="@drawable/penguin_happy_3_9"   android:duration="150"/>
<item android:drawable="@drawable/penguin_happy_3_10"   android:duration="150"/>
<item android:drawable="@drawable/penguin_happy_3_11"   android:duration="150"/>
<item android:drawable="@drawable/penguin_happy_3_12"   android:duration="150"/>
<item android:drawable="@drawable/penguin_happy_3_13"   android:duration="150"/>

</animation-list>

布局文件activity_original.xml
很简单,就是一个ImageView,scr属性为该动画资源:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="${relativePackage}.${activityClass}" >

<ImageView
    android:id="@+id/iv_orginal"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_centerInParent="true"
    android:src="@anim/frame_demo" />

</RelativeLayout>

然后在activity实现动画播放:

ImageView iv_original = (ImageView) findViewById(R.id.iv_orginal);
AnimationDrawable ani = (AnimationDrawable) iv_original.getDrawable();
ani.start();//播放动画

//打印出程序占用的内存
Log.e(TAG, Runtime.getRuntime().totalMemory()/1024 + "k");

以上就是安卓开发中利用普通方法实现帧动画的方式,代码很简单,我们输出该方式播放时整个应用占用的内存大小为6.08M。

四、利用SurfaceView打造自定义帧动画播放框架

首先是我们自定义的动画框架,继承自SurfaceView,
完整代码如下:

public class FrameAnimation extends SurfaceView implements SurfaceHolder.Callback, Runnable {

private SurfaceHolder mSurfaceHolder;

private boolean mIsThreadRunning = true; // 线程运行开关
private boolean mIsDestroy = false;// 是否已经销毁

private int[] mBitmapResourceIds;// 用于播放动画的图片资源数组
private Canvas mCanvas;
private Bitmap mBitmap;// 显示的图片

private int mCurrentIndext;// 当前动画播放的位置
private int mGapTime = 150;// 每帧动画持续存在的时间

private OnFrameFinishedListener mOnFrameFinishedListener;// 动画监听事件

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

public FrameAnimation(Context context, AttributeSet attrs, int defStyle)
{
    super(context, attrs, defStyle);
    mSurfaceHolder = this.getHolder();
    mSurfaceHolder.addCallback(this);// 注册回调方法

    // 白色背景
    setZOrderOnTop(true);
    mSurfaceHolder.setFormat(PixelFormat.TRANSLUCENT);
}

public FrameAnimation(Context context, AttributeSet attrs)
{
    this(context, attrs, 0);
}

@Override
public void surfaceCreated(SurfaceHolder holder)
{
    // 创建surfaceView时启动线程
    // new Thread(this).start();
}

@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
{
}

@Override
public void surfaceDestroyed(SurfaceHolder holder)
{
    // 当surfaceView销毁时, 停止线程的运行. 避免surfaceView销毁了线程还在运行而报错.
    mIsThreadRunning = false;
    try
    {
        Thread.sleep(mGapTime);
    } catch (InterruptedException e)
    {
        e.printStackTrace();
    }

    mIsDestroy = true;
}

/**
 * 制图方法
 */
private void drawView()
{
    // 无资源文件退出
    if (mBitmapResourceIds == null)
    {
        Log.e("frameview", "the bitmapsrcIDs is null");

        mIsThreadRunning = false;

        return;
    }

    // 锁定画布
    mCanvas = mSurfaceHolder.lockCanvas();
    try
    {
        if (mSurfaceHolder != null && mCanvas != null)
        {

            mCanvas.drawColor(Color.WHITE);

            // 如果图片过大可以再次对图片进行二次采样缩放处理
            mBitmap = BitmapFactory.decodeResource(getResources(), mBitmapResourceIds[mCurrentIndext]);
            mCanvas.drawBitmap(mBitmap, (getWidth() - mBitmap.getWidth()) / 2,
                    (getHeight() - mBitmap.getHeight()) / 2, null);

            // 播放到最后一张图片,停止线程
            if (mCurrentIndext == mBitmapResourceIds.length - 1)
            {
                mIsThreadRunning = false;
            }

        }
    } catch (Exception e)
    {
        e.printStackTrace();
    } finally
    {
        mCurrentIndext++;

        if (mCanvas != null)
        {
            // 将画布解锁并显示在屏幕上
            mSurfaceHolder.unlockCanvasAndPost(mCanvas);
        }

        if (mBitmap != null)
        {
            // 收回图片
            mBitmap.recycle();
        }
    }
}

@Override
public void run()
{
    if (mOnFrameFinishedListener != null)
    {
        mOnFrameFinishedListener.onStart();
    }

    // 每隔100ms刷新屏幕
    while (mIsThreadRunning)
    {
        drawView();
        try
        {
            Thread.sleep(mGapTime);
        } catch (Exception e)
        {
            e.printStackTrace();
        }
    }

    if (mOnFrameFinishedListener != null)
    {
        mOnFrameFinishedListener.onStop();
    }
}

/**
 * 开始动画
 */
public void start()
{
    if (!mIsDestroy)
    {
        mCurrentIndext = 0;
        mIsThreadRunning = true;
        new Thread(this).start();
    } else
    {
        // 如果SurfaceHolder已经销毁抛出该异常
        try
        {
            throw new Exception("IllegalArgumentException:Are you sure the SurfaceHolder is not destroyed");
        } catch (Exception e)
        {
            e.printStackTrace();
        }
    }
}

/**
 * 设置动画播放素材
 * @param bitmapResoursIds  图片资源id
 */
public void setBitmapResoursID(int[] bitmapResourceIds)
{
    this.mBitmapResourceIds = bitmapResourceIds;
}

/**
 * 设置每帧时间
 */
public void setGapTime(int gapTime)
{
    this.mGapTime = gapTime;
}

/**
 * 结束动画
 */
public void stop()
{
    mIsThreadRunning = false;
}

/**
 * 继续动画
 */
public void reStart()
{
    mIsThreadRunning = false;
}

/**
 * 设置动画监听器
 */
public void setOnFrameFinisedListener(OnFrameFinishedListener onFrameFinishedListener)
{
    this.mOnFrameFinishedListener = onFrameFinishedListener;
}

/**
 * 动画监听器
 * @author qike
 *
 */
public interface OnFrameFinishedListener {

    /**
     * 动画开始
     */
    void onStart();

    /**
     * 动画结束
     */
    void onStop();
}

/**
 * 当用户点击返回按钮时,停止线程,反转内存溢出
 */
@Override
public boolean onKeyDown(int keyCode, KeyEvent event)
{
    // 当按返回键时,将线程停止,避免surfaceView销毁了,而线程还在运行而报错
    if (keyCode == KeyEvent.KEYCODE_BACK)
    {
        mIsThreadRunning = false;
    }

    return super.onKeyDown(keyCode, event);
}

}

布局文件activity_main.xml,同样很简单:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white"
    tools:context="${relativePackage}.${activityClass}" >

<com.example.frameanidemo.view.FrameAnimation
    android:id="@+id/ani_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

</RelativeLayout>

最后是activity:

public class CustomFrameActivity extends Activity {

    protected static final String TAG = "Custom";

    private FrameAnimation frameView;

    //动画资源文件
    int[] srcId =
    { R.drawable.penguin_happy_3_1, R.drawable.penguin_happy_3_2, R.drawable.penguin_happy_3_3,
        R.drawable.penguin_happy_3_4, R.drawable.penguin_happy_3_5, R.drawable.penguin_happy_3_6,
        R.drawable.penguin_happy_3_7, R.drawable.penguin_happy_3_8, R.drawable.penguin_happy_3_9,
        R.drawable.penguin_happy_3_10, R.drawable.penguin_happy_3_11, R.drawable.penguin_happy_3_12,
        R.drawable.penguin_happy_3_13 };

@Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        frameView = (FrameAnimation) findViewById(R.id.ani_view);
        frameView.setBitmapResoursID(srcId);

        //设置监听事件
        frameView.setOnFrameFinisedListener(new FrameAnimation.OnFrameFinishedListener() {

            @Override
            public void onStop()
            {
                Log.e(TAG, "stop");
            }

            @Override
            public void onStart()
            {
                Log.e(TAG, "start");

                Log.e(TAG, Runtime.getRuntime().totalMemory() / 1024 + "k");
            }
        });

        frameView.start();
    }
}

代码已经附上,看下这种方式所占的内存只有2.8M,比原生的6.08M节省了不是一丁点,这种差距尤其是在大量图片的播放中更是明显,经测试原生帧动画播放超过20张图片就非常危险,而我们自定义的与图片的多寡并无区别:
自定义帧动画所占内存

源码下载:http://download.csdn.net/detail/q649381130/9212135

你可能感兴趣的:(动画,Android开发,oom,内存溢出)