如何解决Android帧动画出现的内存溢出

时间:2022-09-10 10:15:24

这几天在做动画的时候,遇到了一个OOM的问题,特此记录下来。

普通实现

实现一个帧动画,最先想到的就是用animation-list将全部图片按顺序放入,并设置时间间隔和播放模式。然后将该drawable设置给ImageView或Progressbar就OK了。

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot="false"> <item android:drawable="@drawable/smile0" android:duration="30"/>
<item android:drawable="@drawable/smile1" android:duration="30"/>
<item android:drawable="@drawable/smile2" android:duration="30"/>
<item android:drawable="@drawable/smile3" android:duration="30"/>
<item android:drawable="@drawable/smile4" android:duration="30"/>
</animation-list>

但是如果图片太多了,而且每张图片几百K的情况,就会出现OOM的问题。可以参考Stack Overflow上的这个问题Causing OutOfMemoryError in Frame by Frame Animation in Android

造成OOM的原因是因为帧动画从xml中读取图片的时候,一次性读取了所有的图片,并设置给ImageView,所以在图片数量过多和图片过大的时候,就会出现OOM。既然知道了原因,那么解决思路也很简单,就是在进行帧动画显示的时候,不要一下子读取所有的图片,而是需要谁就读取谁。

在github上找到一个例子,tigerjj/FasterAnimationsContainer,具体实现代码如下

public class FasterAnimationsContainer {
private class AnimationFrame{
private int mResourceId;
private int mDuration;
AnimationFrame(int resourceId, int duration){
mResourceId = resourceId;
mDuration = duration;
}
public int getResourceId() {
return mResourceId;
}
public int getDuration() {
return mDuration;
}
}
private ArrayList<AnimationFrame> mAnimationFrames; // list for all frames of animation
private int mIndex; // index of current frame private boolean mShouldRun; // true if the animation should continue running. Used to stop the animation
private boolean mIsRunning; // true if the animation prevents starting the animation twice
private SoftReference<ImageView> mSoftReferenceImageView; // Used to prevent holding ImageView when it should be dead.
private Handler mHandler; // Handler to communication with UIThread private Bitmap mRecycleBitmap; //Bitmap can recycle by inBitmap is SDK Version >=11 // Listeners
private OnAnimationStoppedListener mOnAnimationStoppedListener;
private OnAnimationFrameChangedListener mOnAnimationFrameChangedListener; private FasterAnimationsContainer(ImageView imageView, Context mContext) {
this.mContext = mContext;
init(imageView, mContext);
}; // single instance procedures
private static FasterAnimationsContainer sInstance; private Context mContext; public static FasterAnimationsContainer getInstance(ImageView imageView, Context mContext) {
if (sInstance == null)
sInstance = new FasterAnimationsContainer(imageView, mContext);
sInstance.mRecycleBitmap = null;
return sInstance;
} /**
* initialize imageview and frames
* @param imageView
* @param mContext
*/
public void init(ImageView imageView, Context mContext){
mAnimationFrames = new ArrayList<AnimationFrame>();
mSoftReferenceImageView = new SoftReference<ImageView>(imageView); mHandler = new Handler();
if(mIsRunning == true){
stop();
} mShouldRun = false;
mIsRunning = false; mIndex = -1;
} /**
* add a frame of animation
* @param index index of animation
* @param resId resource id of drawable
* @param interval milliseconds
*/
public void addFrame(int index, int resId, int interval){
mAnimationFrames.add(index, new AnimationFrame(resId, interval));
} /**
* add a frame of animation
* @param resId resource id of drawable
* @param interval milliseconds
*/
public void addFrame(int resId, int interval){
mAnimationFrames.add(new AnimationFrame(resId, interval));
} /**
* add all frames of animation
* @param resId resource id of drawable
* @param interval milliseconds
*/
public void addAllFrames(int resId, int interval){
int[] drawableIds = getData(resId);
for(int drawableId : drawableIds){
mAnimationFrames.add(new AnimationFrame(drawableId, interval));
}
} /**
* 从xml中读取帧数组
* @param resId
* @return
*/
private int[] getData(int resId){
TypedArray array = mContext.getResources().obtainTypedArray(resId); int len = array.length();
int[] intArray = new int[array.length()]; for(int i = 0; i < len; i++){
intArray[i] = array.getResourceId(i, 0);
}
array.recycle();
return intArray;
} /**
* remove a frame with index
* @param index index of animation
*/
public void removeFrame(int index){
mAnimationFrames.remove(index);
} /**
* clear all frames
*/
public void removeAllFrames(){
mAnimationFrames.clear();
} /**
* change a frame of animation
* @param index index of animation
* @param resId resource id of drawable
* @param interval milliseconds
*/
public void replaceFrame(int index, int resId, int interval){
mAnimationFrames.set(index, new AnimationFrame(resId, interval));
} private AnimationFrame getNext() {
mIndex++;
if (mIndex >= mAnimationFrames.size())
mIndex = 0;
return mAnimationFrames.get(mIndex);
} /**
* Listener of animation to detect stopped
*
*/
public interface OnAnimationStoppedListener{
public void onAnimationStopped();
} /**
* Listener of animation to get index
*
*/
public interface OnAnimationFrameChangedListener{
public void onAnimationFrameChanged(int index);
} /**
* set a listener for OnAnimationStoppedListener
* @param listener OnAnimationStoppedListener
*/
public void setOnAnimationStoppedListener(OnAnimationStoppedListener listener){
mOnAnimationStoppedListener = listener;
} /**
* set a listener for OnAnimationFrameChangedListener
* @param listener OnAnimationFrameChangedListener
*/
public void setOnAnimationFrameChangedListener(OnAnimationFrameChangedListener listener){
mOnAnimationFrameChangedListener = listener;
} /**
* Starts the animation
*/
public synchronized void start() {
mShouldRun = true;
if (mIsRunning)
return;
mHandler.post(new FramesSequenceAnimation());
} /**
* Stops the animation
*/
public synchronized void stop() {
mShouldRun = false;
} private class FramesSequenceAnimation implements Runnable{ @Override
public void run() {
ImageView imageView = mSoftReferenceImageView.get();
if (!mShouldRun || imageView == null) {
mIsRunning = false;
if (mOnAnimationStoppedListener != null) {
mOnAnimationStoppedListener.onAnimationStopped();
}
return;
}
mIsRunning = true; if (imageView.isShown()) {
AnimationFrame frame = getNext();
GetImageDrawableTask task = new GetImageDrawableTask(imageView);
task.execute(frame.getResourceId());
// TODO postDelayed after onPostExecute
mHandler.postDelayed(this, frame.getDuration());
}
}
} private class GetImageDrawableTask extends AsyncTask<Integer, Void, Drawable> { private ImageView mImageView; public GetImageDrawableTask(ImageView imageView) {
mImageView = imageView;
} @SuppressLint("NewApi")
@Override
protected Drawable doInBackground(Integer... params) {
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB){
return mContext.getResources().getDrawable(params[0]);
}
BitmapFactory.Options options = new BitmapFactory.Options();
options.inMutable = true;
if (mRecycleBitmap != null)
options.inBitmap = mRecycleBitmap;
mRecycleBitmap = BitmapFactory.decodeResource(mContext.getResources(), params[0], options);
BitmapDrawable drawable = new BitmapDrawable(mContext.getResources(),mRecycleBitmap);
return drawable;
} @Override
protected void onPostExecute(Drawable result) {
super.onPostExecute(result);
if(result!=null) mImageView.setImageDrawable(result);
if (mOnAnimationFrameChangedListener != null)
mOnAnimationFrameChangedListener.onAnimationFrameChanged(mIndex);
} }