在Android中的逐帧动画中导致OutOfMemoryError

Posted

技术标签:

【中文标题】在Android中的逐帧动画中导致OutOfMemoryError【英文标题】:Causing OutOfMemoryError in Frame by Frame Animation in Android 【发布时间】:2012-01-31 07:34:41 【问题描述】:

我的资源/可绘制文件夹中有很多图像作为框架(比如说大约 200 个)。并使用这些图像我想运行动画。最长的动画是 80 帧。我可以通过单击某些按钮成功运行动画,但是对于某些动画,它给了我 OutOfMemoryError 说 VM 无法提供这样的内存。它超出了 VM 预算。我计算所有图像的大小约为 10MB。每个图像的大小为 320x480 像素。

我尝试谷歌搜索,发现我需要使用 System.gc() 方法显式调用垃圾收集器。我已经这样做了,但我仍然遇到一些时间错误的记忆。谁能帮我解决这个问题。

一些代码:-

ImageView img = (ImageView)findViewById(R.id.xxx);
img.setBackgroundResource(R.anim.angry_tail_animation);
AnimationDrawable mailAnimation = (AnimationDrawable) img.getBackground();
MediaPlayer player = MediaPlayer.create(this.getApplicationContext(), R.raw.angry);
    if(mailAnimation.isRunning()) 
    mailAnimation.stop();
    mailAnimation.start();
        if (player.isPlaying()) 
        player.stop();
        player.start();
    
    else 
        player.start();
    

else 
    mailAnimation.start();
        if (player.isPlaying()) 
        player.stop();
        player.start();
    
    else 
        player.start();
    

这是我在点击按钮时编写的代码.....

res/drawable/anim 中的资源文件

<?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/cat_angry0000" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0001" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0002" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0003" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0004" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0005" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0006" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0007" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0008" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0009" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0010" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0011" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0012" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0013" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0014" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0015" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0016" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0017" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0018" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0019" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0020" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0021" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0022" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0023" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0024" android:duration="50"/>

<item android:drawable="@drawable/cat_angry0025" android:duration="50"/>

</animation-list>

** 以上是 setBackgroundResource 中使用的资源文件,同样我还有 10 个文件用于其他不同的动画。 **

错误日志

01-16 22:23:41.594: E/AndroidRuntime(399): FATAL EXCEPTION: main
01-16 22:23:41.594: E/AndroidRuntime(399): java.lang.IllegalStateException: Could not execute method of the activity
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.view.View$1.onClick(View.java:2144)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.view.View.performClick(View.java:2485)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.view.View$PerformClick.run(View.java:9080)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.os.Handler.handleCallback(Handler.java:587)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.os.Handler.dispatchMessage(Handler.java:92)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.os.Looper.loop(Looper.java:123)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.app.ActivityThread.main(ActivityThread.java:3683)
01-16 22:23:41.594: E/AndroidRuntime(399):  at java.lang.reflect.Method.invokeNative(Native Method)
01-16 22:23:41.594: E/AndroidRuntime(399):  at java.lang.reflect.Method.invoke(Method.java:507)
01-16 22:23:41.594: E/AndroidRuntime(399):  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:839)
01-16 22:23:41.594: E/AndroidRuntime(399):  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:597)
01-16 22:23:41.594: E/AndroidRuntime(399):  at dalvik.system.NativeStart.main(Native Method)
01-16 22:23:41.594: E/AndroidRuntime(399): Caused by: java.lang.reflect.InvocationTargetException
01-16 22:23:41.594: E/AndroidRuntime(399):  at java.lang.reflect.Method.invokeNative(Native Method)
01-16 22:23:41.594: E/AndroidRuntime(399):  at java.lang.reflect.Method.invoke(Method.java:507)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.view.View$1.onClick(View.java:2139)
01-16 22:23:41.594: E/AndroidRuntime(399):  ... 11 more
01-16 22:23:41.594: E/AndroidRuntime(399): Caused by: java.lang.OutOfMemoryError: bitmap size exceeds VM budget
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:460)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.graphics.BitmapFactory.decodeResourceStream(BitmapFactory.java:336)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.graphics.drawable.Drawable.createFromResourceStream(Drawable.java:697)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.content.res.Resources.loadDrawable(Resources.java:1709)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.content.res.Resources.getDrawable(Resources.java:581)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.graphics.drawable.AnimationDrawable.inflate(AnimationDrawable.java:267)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.graphics.drawable.Drawable.createFromXmlInner(Drawable.java:787)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.graphics.drawable.Drawable.createFromXml(Drawable.java:728)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.content.res.Resources.loadDrawable(Resources.java:1694)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.content.res.Resources.getDrawable(Resources.java:581)
01-16 22:23:41.594: E/AndroidRuntime(399):  at android.view.View.setBackgroundResource(View.java:7533)
01-16 22:23:41.594: E/AndroidRuntime(399):  at talking.cat.CatActivity.middleButtonClicked(CatActivity.java:83)

同样的,我有不同的按钮用于不同的动画...... 谢谢

【问题讨论】:

是的哥们,这是android的一个大问题。我总是面对它,但没有找到特别的解决方案.. ;) @AndroidKiller 感谢您的回复,但我需要为此找到一些东西...... 你能在这里贴一些代码吗……这可能有助于我们更好地理解…… 你在测试什么?真正的设备,模拟器? @MikeIsrael 我现在正在模拟器上测试它... 【参考方案1】:

这是sdk的大问题,但可以通过使用线程同时加载位图图像而不是同时加载整个图像来解决。

【讨论】:

我现在在问题中添加资源文件代码。感谢您的快速回复。请让我知道我在哪里犯了错误。 @ JAFFER,感谢您的回答,但使用线程如何加载我的图像?你能否给我一些提示或提示,我应该如何继续。我在许多网站上发现使用 Bitmap 或 BitmapFactory.options 但我对如何使用这些类一无所知。【参考方案2】:

我假设您的动画帧图像是压缩的(PNG 或 JPG)。压缩后的大小对于计算显示它们需要多少内存没有用。为此,您需要考虑未压缩的大小。这将是像素数 (320x480) 乘以每个像素的字节数,通常为 4(32 位)。那么,对于您的图像,每个图像将是 614,400 字节。对于您提供的 26 帧动画示例,总共需要 15,974,400 字节来保存所有帧的原始位图数据,这还不包括对象开销。

查看AnimationDrawable 的源代码,它似乎一次将所有帧加载到内存中,为了获得良好的性能,它基本上必须这样做。

是否可以分配这么多内存取决于系统。我至少建议在真实设备而不是模拟器上尝试这个。您也可以尝试调整模拟器的可用 RAM 大小,但这只是猜测。

有多种方法可以使用 BitmapFactory.inPreferredConfig 以更节省内存的格式(例如 RGB 565(而不是 ARGB 8888))加载位图。这会节省一些空间,但可能还不够。

如果您不能一次分配那么多内存,则必须考虑其他选项。大多数高性能图形应用程序(例如游戏)从较小的图形(精灵)或 2D 或 3D 图元(矩形、三角形)的组合中绘制图形。为每一帧绘制全屏位图实际上与渲染视频相同;不一定是最有效的。

动画的全部内容是否会随着每一帧而改变?另一种优化可能是仅对实际更改的部分进行动画处理,并切碎您的位图以解决此问题。

总而言之,您需要找到一种使用更少内存来绘制动画的方法。有很多选择,但这在很大程度上取决于您的动画需要的外观。

【讨论】:

@LYRICSBOY,感谢您的回复。你可能听说过安卓和其他人的会说话的汤姆猫游戏。我想要在我的应用程序中完全相同的东西。正如我之前所说,我有大小为 320x480 像素的帧,所以我在谷歌上搜索精灵,发现精灵对小尺寸图像有用,而不是大 png。对于 Bitmap Factory,我不知道如何将所有这些帧加载到一个 Bitmap 对象中,或者如何使用 Bitmap 类而不是 AnimationDrawable 来运行我的动画。 看起来您可以在代码中使用 AnimationDrawable(而不是 XML)并使用 addFrame(Drawable, int) 传递您自己的可绘制对象,也可以使用 BitmapDrawable 在代码中创建以包装使用 @ 创建的 Bitmaps 987654327@。或者,您可以尝试创建自己的 DrawableContainer 子类(AnimationDrawable 的超类)更适合您的需求。或者,您可以创建一个游戏风格的绘图循环,尽可能频繁地触发,但不会比您期望的帧速率快,并从某种智能缓存中的资源加载位图。 @ LYRICSBOY,你能指导我如何在我的应用程序中使用 BitmapFactory 、 BitmapDrawable 和 Bitmap,因为我以前从未使用过这些东西。我在此 AnimationDrawable 之前的代码中使用了 addFrame ,但我遇到了同样的错误。但我没有使用线程来运行动画。这有什么区别吗? this article相关的代码有一些使用BitmapFactory的例子。快速搜索应该会找到更多。【参考方案3】:

我遇到了同样的问题。 Android 一次加载所有可绘制对象,因此具有多帧的动画会导致此错误。

我最终创建了自己的简单序列动画:

public class AnimationsContainer 
    public int FPS = 30;  // animation FPS

    // single instance procedures
    private static AnimationsContainer mInstance;

    private AnimationsContainer() 
    ;

    public static AnimationsContainer getInstance() 
        if (mInstance == null)
            mInstance = new AnimationsContainer();
        return mInstance;
    

    // animation progress dialog frames
    private int[] mProgressAnimFrames =  R.drawable.logo_30001, R.drawable.logo_30002, R.drawable.logo_30003 ;

    // animation splash screen frames
    private int[] mSplashAnimFrames =  R.drawable.logo_ding200480001, R.drawable.logo_ding200480002 ;


    /**
     * @param imageView 
     * @return progress dialog animation
     */
    public FramesSequenceAnimation createProgressDialogAnim(ImageView imageView) 
        return new FramesSequenceAnimation(imageView, mProgressAnimFrames);
    

    /**
     * @param imageView
     * @return splash screen animation
     */
    public FramesSequenceAnimation createSplashAnim(ImageView imageView) 
        return new FramesSequenceAnimation(imageView, mSplashAnimFrames);
    

    /**
     * AnimationPlayer. Plays animation frames sequence in loop
     */
public class FramesSequenceAnimation 
    private int[] mFrames; // animation frames
    private int mIndex; // current frame
    private boolean mShouldRun; // true if the animation should continue running. Used to stop the animation
    private boolean mIsRunning; // true if the animation currently running. prevents starting the animation twice
    private SoftReference<ImageView> mSoftReferenceImageView; // Used to prevent holding ImageView when it should be dead.
    private Handler mHandler;
    private int mDelayMillis;
    private OnAnimationStoppedListener mOnAnimationStoppedListener;

    private Bitmap mBitmap = null;
    private BitmapFactory.Options mBitmapOptions;

    public FramesSequenceAnimation(ImageView imageView, int[] frames, int fps) 
        mHandler = new Handler();
        mFrames = frames;
        mIndex = -1;
        mSoftReferenceImageView = new SoftReference<ImageView>(imageView);
        mShouldRun = false;
        mIsRunning = false;
        mDelayMillis = 1000 / fps;

        imageView.setImageResource(mFrames[0]);

        // use in place bitmap to save GC work (when animation images are the same size & type)
        if (Build.VERSION.SDK_INT >= 11) 
            Bitmap bmp = ((BitmapDrawable) imageView.getDrawable()).getBitmap();
            int width = bmp.getWidth();
            int height = bmp.getHeight();
            Bitmap.Config config = bmp.getConfig();
            mBitmap = Bitmap.createBitmap(width, height, config);
            mBitmapOptions = new BitmapFactory.Options();
            // setup bitmap reuse options. 
            mBitmapOptions.inBitmap = mBitmap;
            mBitmapOptions.inMutable = true;
            mBitmapOptions.inSampleSize = 1;
        
    

    private int getNext() 
        mIndex++;
        if (mIndex >= mFrames.length)
            mIndex = 0;
        return mFrames[mIndex];
    

    /**
     * Starts the animation
     */
    public synchronized void start() 
        mShouldRun = true;
        if (mIsRunning)
            return;

        Runnable runnable = new Runnable() 
            @Override
            public void run() 
                ImageView imageView = mSoftReferenceImageView.get();
                if (!mShouldRun || imageView == null) 
                    mIsRunning = false;
                    if (mOnAnimationStoppedListener != null) 
                        mOnAnimationStoppedListener.AnimationStopped();
                    
                    return;
                

                mIsRunning = true;
                mHandler.postDelayed(this, mDelayMillis);

                if (imageView.isShown()) 
                    int imageRes = getNext();
                    if (mBitmap != null)  // so Build.VERSION.SDK_INT >= 11
                        Bitmap bitmap = null;
                        try 
                            bitmap = BitmapFactory.decodeResource(imageView.getResources(), imageRes, mBitmapOptions);
                         catch (Exception e) 
                            e.printStackTrace();
                        
                        if (bitmap != null) 
                            imageView.setImageBitmap(bitmap);
                         else 
                            imageView.setImageResource(imageRes);
                            mBitmap.recycle();
                            mBitmap = null;
                        
                     else 
                        imageView.setImageResource(imageRes);
                    
                

            
        ;

        mHandler.post(runnable);
    

        /**
         * Stops the animation
         */
        public synchronized void stop() 
            mShouldRun = false;
        
    

用法:

FramesSequenceAnimation anim = AnimationsContainer.getInstance().createSplashAnim(mSplashImageView);
anim.start();
别忘了停下来……

【讨论】:

感谢您的回答。我一定会试试这个。希望它能解决我的问题。再次感谢 真的很棒。对于 OnAnimationStoppedListener,我添加了一个只有一个方法 void AnimationStopped() 的接口。我还对其进行了一些更改,以便我可以制作一个单一的动画。 我已经改进了这个类并上传到了github。 oom with frame动画有问题的请点击here。 由于某种原因,动画不流畅,并且花费的时间超出了应有的时间。我的数组是动态构建的(在主线程上),所以我已经切换到 ArrayList(~200 个元素),我的间隔是 34 毫秒(30 fps)。有什么建议吗? 如果动画运行缓慢,可能是因为帧是实时加载的...我的建议是将图像设置为相同的大小和类型,并确保“if (Build .VERSION.SDK_INT >= 11) "... 代码正在运行 - 它应该为新图像使用相同的内存分配并为您提供更好的性能【参考方案4】:

我创建了一个动画类,它根据传入的可绘制资源和帧持续时间显示帧。

 protected class SceneAnimation
    private ImageView mImageView;
    private int[] mFrameRess;
    private int[] mDurations;
    private int mDuration;

    private int mLastFrameNo;
    private long mBreakDelay;

 public SceneAnimation(ImageView pImageView, int[] pFrameRess, int[] pDurations)
        mImageView = pImageView;
        mFrameRess = pFrameRess;
        mDurations = pDurations;
        mLastFrameNo = pFrameRess.length - 1;

        mImageView.setImageResource(mFrameRess[0]);
        play(1);
    

    public SceneAnimation(ImageView pImageView, int[] pFrameRess, int pDuration)
        mImageView = pImageView;
        mFrameRess = pFrameRess;
        mDuration = pDuration;
        mLastFrameNo = pFrameRess.length - 1;

        mImageView.setImageResource(mFrameRess[0]);
        playConstant(1);
    

    public SceneAnimation(ImageView pImageView, int[] pFrameRess, int pDuration, long pBreakDelay)            
        mImageView = pImageView;
        mFrameRess = pFrameRess;
        mDuration = pDuration;
        mLastFrameNo = pFrameRess.length - 1;
        mBreakDelay = pBreakDelay;

        mImageView.setImageResource(mFrameRess[0]);
        playConstant(1);
    


    private void play(final int pFrameNo)
        mImageView.postDelayed(new Runnable()
            public void run() 
                mImageView.setImageResource(mFrameRess[pFrameNo]);
                if(pFrameNo == mLastFrameNo)
                    play(0);
                else
                    play(pFrameNo + 1);
            
        , mDurations[pFrameNo]);
    


    private void playConstant(final int pFrameNo)
        mImageView.postDelayed(new Runnable()
            public void run()                     
                mImageView.setImageResource(mFrameRess[pFrameNo]);

                if(pFrameNo == mLastFrameNo)
                    playConstant(0);
                else
                    playConstant(pFrameNo + 1);
            
        , pFrameNo==mLastFrameNo && mBreakDelay>0 ? mBreakDelay : mDuration);
            
;

它是这样使用的:

 private ImageView mTapScreenTextAnimImgView;    
private final int[] mTapScreenTextAnimRes = R.drawable.tap0001_b, R.drawable.tap0002_b, R.drawable.tap0003_b, 
        R.drawable.tap0004_b, R.drawable.tap0005_b, R.drawable.tap0006_b, R.drawable.tap0005_b, R.drawable.tap0004_b,
        R.drawable.tap0003_b, R.drawable.tap0002_b, R.drawable.tap0001_b;
private final int mTapScreenTextAnimDuration = 100;
private final int mTapScreenTextAnimBreak = 500;

在 onCreate 中:

 mTapScreenTextAnimImgView = (ImageView) findViewById(R.id.scene1AnimBottom);
    new SceneAnimation(mTapScreenTextAnimImgView, mTapScreenTextAnimRes, mTapScreenTextAnimDuration, mTapScreenTextAnimBreak);

【讨论】:

感谢您的回复,我会试试这个,希望它对我有用。【参考方案5】:

我在这方面花了很多时间,有两种不同的解决方案,都很好..

首先,问题: 1) Android 以未压缩的位图格式将所有图像加载到 RAM 中。 2) Android 使用资源缩放,所以在 xxxhdpi 显示屏的手机上(如 LG G3),每一帧都占用 TON 的空间,所以你很快就会耗尽 RAM。

解决方案 #1

1) 绕过 Android 的资源缩放。 2) 将所有文件的字节数组存储在内存中(这些文件很小,尤其是对于 JPEG)。 3) 逐帧生成位图,因此几乎不可能耗尽内存。

缺点:当 Android 为新位图分配内存并回收旧位图时,它会向您的日志发送垃圾邮件。它在旧设备(Galaxy S1)上也表现糟糕,但在当前预算手机上表现良好(阅读:我在 BestBuy 买的 10 美元阿尔卡特 C1)。下面的第二种解决方案在旧设备上表现更好,但在某些情况下仍可能耗尽 RAM。

public class MyAnimationDrawable 
public static class MyFrame 
    byte[] bytes;
    int duration;
    Drawable drawable;
    boolean isReady = false;



public interface OnDrawableLoadedListener 
    public void onDrawableLoaded(List<MyFrame> myFrames);


public static void loadRaw(final int resourceId, final Context context, final OnDrawableLoadedListener onDrawableLoadedListener) 
    loadFromXml(resourceId, context, onDrawableLoadedListener);


private static void loadFromXml(final int resourceId, final Context context, final OnDrawableLoadedListener onDrawableLoadedListener) 
    new Thread(new Runnable() 
        @Override
        public void run() 
            final ArrayList<MyFrame> myFrames = new ArrayList<>();

            XmlResourceParser parser = context.getResources().getXml(resourceId);

            try 
                int eventType = parser.getEventType();
                while (eventType != XmlPullParser.END_DOCUMENT) 
                    if (eventType == XmlPullParser.START_DOCUMENT) 

                     else if (eventType == XmlPullParser.START_TAG) 

                        if (parser.getName().equals("item")) 
                            byte[] bytes = null;
                            int duration = 1000;

                            for (int i=0; i<parser.getAttributeCount(); i++) 
                                if (parser.getAttributeName(i).equals("drawable")) 
                                    int resId = Integer.parseInt(parser.getAttributeValue(i).substring(1));
                                    bytes = IOUtils.toByteArray(context.getResources().openRawResource(resId));
                                
                                else if (parser.getAttributeName(i).equals("duration")) 
                                    duration = parser.getAttributeIntValue(i, 1000);
                                
                            

                            MyFrame myFrame = new MyFrame();
                            myFrame.bytes = bytes;
                            myFrame.duration = duration;
                            myFrames.add(myFrame);
                        

                     else if (eventType == XmlPullParser.END_TAG) 

                     else if (eventType == XmlPullParser.TEXT) 

                    

                    eventType = parser.next();
                
            
            catch (IOException | XmlPullParserException e) 
                e.printStackTrace();
            

            // Run on UI Thread
            new Handler(context.getMainLooper()).post(new Runnable() 
                @Override
                public void run() 
                    if (onDrawableLoadedListener != null) 
                        onDrawableLoadedListener.onDrawableLoaded(myFrames);
                    
                
            );
        
    ).run();


public static void animateRawManually(int resourceId, final ImageView imageView, final Runnable onStart, final Runnable onComplete) 
    loadRaw(resourceId, imageView.getContext(), new OnDrawableLoadedListener() 
        @Override
        public void onDrawableLoaded(List<MyFrame> myFrames) 
            if (onStart != null) 
                onStart.run();
            

            animateRawManually(myFrames, imageView, onComplete);
        
    );


public static void animateRawManually(List<MyFrame> myFrames, ImageView imageView, Runnable onComplete) 
    animateRawManually(myFrames, imageView, onComplete, 0);


private static void animateRawManually(final List<MyFrame> myFrames, final ImageView imageView, final Runnable onComplete, final int frameNumber) 
    final MyFrame thisFrame = myFrames.get(frameNumber);

    if (frameNumber == 0) 
        thisFrame.drawable = new BitmapDrawable(imageView.getContext().getResources(), BitmapFactory.decodeByteArray(thisFrame.bytes, 0, thisFrame.bytes.length));
    
    else 
        MyFrame previousFrame = myFrames.get(frameNumber - 1);
        ((BitmapDrawable) previousFrame.drawable).getBitmap().recycle();
        previousFrame.drawable = null;
        previousFrame.isReady = false;
    

    imageView.setImageDrawable(thisFrame.drawable);
    new Handler().postDelayed(new Runnable() 
        @Override
        public void run() 
            // Make sure ImageView hasn't been changed to a different Image in this time
            if (imageView.getDrawable() == thisFrame.drawable) 
                if (frameNumber + 1 < myFrames.size()) 
                    MyFrame nextFrame = myFrames.get(frameNumber+1);

                    if (nextFrame.isReady) 
                        // Animate next frame
                        animateRawManually(myFrames, imageView, onComplete, frameNumber + 1);
                    
                    else 
                        nextFrame.isReady = true;
                    
                
                else 
                    if (onComplete != null) 
                        onComplete.run();
                    
                
            
        
    , thisFrame.duration);

    // Load next frame
    if (frameNumber + 1 < myFrames.size()) 
        new Thread(new Runnable() 
            @Override
            public void run() 
                MyFrame nextFrame = myFrames.get(frameNumber+1);
                nextFrame.drawable = new BitmapDrawable(imageView.getContext().getResources(), BitmapFactory.decodeByteArray(nextFrame.bytes, 0, nextFrame.bytes.length));
                if (nextFrame.isReady) 
                    // Animate next frame
                    animateRawManually(myFrames, imageView, onComplete, frameNumber + 1);
                
                else 
                    nextFrame.isReady = true;
                

            
        ).run();
    


** 解决方案 #2 **

它加载 XML 资源,对其进行解析并加载原始资源 - 从而绕过 Android 的资源缩放(导致大多数 OutOfMemoryExceptions),并创建一个 AnimationDrawable。

优势:在旧设备(例如 Galaxy S1)上表现更好

缺点:仍然可能耗尽 RAM,因为它将所有未压缩的位图保存在内存中(但它们更小,因为它们没有像 Android 通常缩放图像那样缩放)

public static void animateManuallyFromRawResource(int animationDrawableResourceId, ImageView imageView, Runnable onStart, Runnable onComplete) 
    AnimationDrawable animationDrawable = new AnimationDrawable();

    XmlResourceParser parser = imageView.getContext().getResources().getXml(animationDrawableResourceId);

    try 
        int eventType = parser.getEventType();
        while (eventType != XmlPullParser.END_DOCUMENT) 
            if (eventType == XmlPullParser.START_DOCUMENT) 

             else if (eventType == XmlPullParser.START_TAG) 

                if (parser.getName().equals("item")) 
                    Drawable drawable = null;
                    int duration = 1000;

                    for (int i=0; i<parser.getAttributeCount(); i++) 
                        if (parser.getAttributeName(i).equals("drawable")) 
                            int resId = Integer.parseInt(parser.getAttributeValue(i).substring(1));
                            byte[] bytes = IoUtils.readBytes(imageView.getContext().getResources().openRawResource(resId));
                            drawable = new BitmapDrawable(imageView.getContext().getResources(), BitmapFactory.decodeByteArray(bytes, 0, bytes.length));
                        
                        else if (parser.getAttributeName(i).equals("duration")) 
                            duration = parser.getAttributeIntValue(i, 66);
                        
                    

                    animationDrawable.addFrame(drawable, duration);
                

             else if (eventType == XmlPullParser.END_TAG) 

             else if (eventType == XmlPullParser.TEXT) 

            

            eventType = parser.next();
        
    
    catch (IOException | XmlPullParserException e) 
        e.printStackTrace();
    

    if (onStart != null) 
        onStart.run();
    
    animateDrawableManually(animationDrawable, imageView, onComplete, 0);


private static void animateDrawableManually(final AnimationDrawable animationDrawable, final ImageView imageView, final Runnable onComplete, final int frameNumber) 
    final Drawable frame = animationDrawable.getFrame(frameNumber);
    imageView.setImageDrawable(frame);
    new Handler().postDelayed(new Runnable() 
        @Override
        public void run() 
            // Make sure ImageView hasn't been changed to a different Image in this time
            if (imageView.getDrawable() == frame) 
                if (frameNumber + 1 < animationDrawable.getNumberOfFrames()) 
                    // Animate next frame
                    animateDrawableManually(animationDrawable, imageView, onComplete, frameNumber + 1);
                
                else 
                    // Animation complete
                    if (onComplete != null) 
                        onComplete.run();
                    
                
            
        
    , animationDrawable.getDuration(frameNumber));

如果您仍然有内存问题,请使用较小的图像...或存储资源名称 + 持续时间,并在每一帧上生成字节数组 + Drawable。这几乎肯定会导致帧之间出现过多的斩波,但使用的 RAM 几乎为零。

【讨论】:

解决方案 1 是我在数小时内找到的最佳解决方案。刚刚更改: bytes = IoUtils.readBytes(context.getResources().openRawResource(resId)); To: bytes = IOUtils.toByteArray(context.getResources().openRawResource(resId)); 感谢@MeerschweinBob 的捕获,我一直在引用我自己的课程。我已经更新了使用标准 IOUtils 的答案。我很高兴它(否则)帮助了你。 为什么在第一个解决方案中,您在 animateRawManually 中执行下一帧两次。一次在 Handler.postDelayed 和一次在 Thread 中? @StevenL 你能解释一下 isReady 布尔值吗?它有什么用? 您能否举例说明如何使用您的解决方案?那些不太精通的人可能不知道如何开始编写代码。【参考方案6】:

我遇到了这个问题并通过以下两件事解决了它:

    将动画图像的分辨率减半...未压缩字节大小的 1/4。 将图像放在 drawable-nodpi 文件夹中,这样它们就不会被 Android 放大。

在执行第 1 步后,我的动画仍然无法在某些手机上加载。 第 2 步让它在这些手机上运行。

希望这可以节省其他人一些时间。

编辑:转到播放 AnimationDrawable 的 Activity 后,我仍然遇到崩溃,但我现在可以正常工作了。以下是我做的其他事情:

    不要在 xml 中使用动画列表。而是在每次需要使用 AnimationDrawable 时创建它。否则,下次您从资源中加载可绘制动画时,它仍会尝试使用您最终回收的位图。 在使用完 AnimationDrawable 后回收位图。这就是释放内存的魔法。 使用 Android 设备监视器监控堆中分配的字节。

这是我用于创建 AnimationDrawable 的代码:

    protected AnimationDrawable CreateLoadingAnimationDrawable()
    
        AnimationDrawable animation = new AnimationDrawable ();
        animation.OneShot = false;
        for (int i = 0; i < kNumberOfFrames; ++i) 
            int index = (i * 2) + 1;
            string stringIndex = index.ToString ("00");
            string bitmapStringId = kBaseAnimationName + stringIndex;
            int resID = this.Resources.GetIdentifier (bitmapStringId, "drawable", this.PackageName);
            Bitmap bitmap = BitmapFactory.DecodeResource (this.Resources, resID);
            BitmapDrawable frame = new BitmapDrawable (bitmap);
            //Drawable frame = Resources.GetDrawable (resID);
            animation.AddFrame (frame, 111);
        
        return animation;
    

以及在您使用完位图后释放位图的代码。您可以在 OnPause 或 OnDestroy 中执行此操作。 _loadingAnimation 是我在上面创建的 AnimationDrawable。在这种情况下,我很想知道 SetCallback() 为您做了什么。我刚刚从 SO 上的其他地方复制了它。

        if (_loadingAnimation != null) 
            _loadingAnimation.Stop ();
            _loadingImageView.SetBackgroundResource (Resource.Drawable.loading_anim_full7001);
            for (int i = 0; i < _loadingAnimation.NumberOfFrames; ++i) 
                BitmapDrawable frame = _loadingAnimation.GetFrame (i) as BitmapDrawable;
                if (frame != null) 
                    Android.Graphics.Bitmap bitmap = frame.Bitmap;
                    bitmap.Recycle ();
                    frame.SetCallback(null);
                
            
            _loadingAnimation.SetCallback(null);
            _loadingAnimation = null;
        

泰德

【讨论】:

这对我来说非常有效,并且是最简单的实现。谢谢! 我很高兴这对某人有所帮助! @tmrog 。请您也在这里回答我。 ***.com/q/45709531/3671748 我错过了什么,或者代码的奇怪大写是怎么回事? string 而不是 String 等?【参考方案7】:

我通过粗暴地降低帧速率并缩小 gimp 中的图像来解决我的 outOfMemoryError 问题。根据您所做的事情,您的 fps 可能会比您预期的低很多。

【讨论】:

【参考方案8】:

我已经通过将所有图像放入数组中并在显示每个图像后使用延迟来解决这个问题。 res/string 中的图像源数组 <!-- Array table for the pictures to show on the spinner--> <array name="spinner_list"> <item>@drawable/arrows_loop__00000_org</item> <item>@drawable/arrows_loop__00005_org</item> <item >@drawable/arrows_loop__00010_org</item> <item>@drawable/arrows_loop__00015_org</item> <item >@drawable/arrows_loop__00020_org</item> <item >@drawable/arrows_loop__00025_org</item> . . . </array> 我声明了微调器 imageView 私有静态 ImageView 图像微调器;

然后在我的课堂上我称之为:

      final TypedArray imgs = getResources().obtainTypedArray(R.array.spinner_list);
    runimage(imgs, imgs.length());

然后在 runimage 上执行延迟循环,如下所示:

        /* handle the spinner frame by frame */

public void runimage(final TypedArray array, int index)

  int size = array.length();

    if(index<size) // show in sequence the images

    final int localindex= index;

        handler.postDelayed(new Runnable() 
            public void run() 
                imagespinner.setImageResource(array.getResourceId(localindex, -1));// find the picture to show
                runimage(array,(localindex+1));// because use final arg need to do the increase inside
            
        , 55);

    
    else // after show all images go ahead
    
        textview2.setVisibility(View.VISIBLE);
        handler.postDelayed(myRunnablewait, 2000); // make some time to see text before go to ather fragment
    


所以我在 imagespinner 上以 55 毫秒的延迟运行所有图像。完成后做下一项工作。

【讨论】:

【参考方案9】:

与其他答案类似,使用 rxjava:

public final class RxSequenceAnimation 
    private static final int[] PNG_RESOURCES = new int[]
            R.drawable.sequence_frame_00,
            R.drawable.sequence_frame_01,
            R.drawable.sequence_frame_02
    ;
    private static final String TAG = "rx-seq-anim";
    private final Resources mResource;
    private final ImageView mImageView;
    private final byte[][] RAW_PNG_DATA = new byte[PNG_RESOURCES.length][];
    private final byte[] buff = new byte[1024];
    private Subscription sub;

    public RxSequenceAnimation(Resources resources, ImageView imageView) 
        mResource = resources;
        mImageView = imageView;
    

    public void start() 
        sub = Observable
                .interval(16, TimeUnit.MILLISECONDS)
                .map(new Func1<Long, Bitmap>() 
                    @Override
                    public Bitmap call(Long l) 
                        int i = (int) (l % PNG_RESOURCES.length);
                        if (RAW_PNG_DATA[i] == null) 
                            // read raw png data (compressed) if not read already into RAM
                            try 
                                RAW_PNG_DATA[i] = read(PNG_RESOURCES[i]);
                             catch (IOException e) 
                                Log.e(TAG, "IOException " + String.valueOf(e));
                            
                            Log.d(TAG, "decoded " + i + " size " + RAW_PNG_DATA[i].length);
                        
                        // decode directly from RAM - only one full blown bitmap is in RAM at a time
                        return BitmapFactory.decodeByteArray(RAW_PNG_DATA[i], 0, RAW_PNG_DATA[i].length);
                    
                )
                .subscribeOn(Schedulers.newThread())
                .onBackpressureDrop()
                .observeOn(AndroidSchedulers.mainThread())
                .doOnNext(new Action1<Bitmap>() 
                    @Override
                    public void call(Bitmap b) 
                        mImageView.setImageBitmap(b);
                    
                )
                .subscribe(LogErrorSubscriber.newInstance(TAG));
    

    public void stop() 
        if (sub != null) 
            sub.unsubscribe();
        
    

    private byte[] read(int resId) throws IOException 
        return streamToByteArray(inputStream(resId));
    

    private InputStream inputStream(int id) 
        return mResource.openRawResource(id);
    

    private byte[] streamToByteArray(InputStream is) throws IOException 
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int i;
        while ((i = is.read(buff, 0, buff.length)) > 0) 
            baos.write(buff, 0, i);
        
        byte[] bytes = baos.toByteArray();
        is.close();
        return bytes;
    

【讨论】:

【参考方案10】:

我将解决方案移植到 Xamarin Android 并进行了一些改进。

它适用于方向变化,特别是宽度和高度约为 300 的图像(图像越大,加载图像所需的时间越长,闪烁越大)。

using Android.Content;
using Android.Graphics;
using Android.OS;
using Android.Widget;
using System;

namespace ...Droid.Util

    public class FramesSequenceAnimation
    
        private int[] animationFrames;
        private int currentFrame;
        private bool shouldRun;   // true if the animation should continue running. Used to stop the animation
        private bool isRunning;   // true if the animation currently running. prevents starting the animation twice
        private ImageView imageview;
        private Handler handler;
        private int delayMillis;
        private bool oneShot = false;
        private FramesSequenceAnimationListener onAnimationStoppedListener;
        private Bitmap bitmap = null;
        private BitmapFactory.Options bitmapOptions;
        private Action action;

        private static object Lock = new object();

        public interface FramesSequenceAnimationListener
        
            void AnimationStopped();
        

        public void SetFramesSequenceAnimationListener(FramesSequenceAnimationListener onAnimationStoppedListener)
        
            this.onAnimationStoppedListener = onAnimationStoppedListener;
        

        public int GetCurrentFrame()
        
            return currentFrame;
        

        public void SetCurrentFrame(int currentFrame)
        
            this.currentFrame = currentFrame;
        

        public FramesSequenceAnimation(FramesSequenceAnimationListener onAnimationStoppedListener, ImageView imageview, int[] animationFrames, int fps)
        
            this.onAnimationStoppedListener = onAnimationStoppedListener;
            this.imageview = imageview;
            this.animationFrames = animationFrames;

            delayMillis = 1000 / fps;

            currentFrame = -1;
            shouldRun = false;
            isRunning = false;
            handler = new Handler();
            imageview.SetImageResource(this.animationFrames[0]);

            //// use in place bitmap to save GC work (when animation images are the same size & type)
            //if (Build.VERSION.SdkInt >= BuildVersionCodes.Honeycomb)
            //
            //    Bitmap bmp = ((BitmapDrawable)imageview.Drawable).Bitmap;
            //    int width = bmp.Width;
            //    int height = bmp.Height;
            //    Bitmap.Config config = bmp.GetConfig();
            //    bitmap = Bitmap.CreateBitmap(width, height, config);
            //    bitmapOptions = new BitmapFactory.Options(); // setup bitmap reuse options
            //    bitmapOptions.InBitmap = bitmap; // reuse this bitmap when loading content
            //    bitmapOptions.InMutable = true;
            //    bitmapOptions.InSampleSize = 1;
            //

            bitmapOptions = newOptions();
            bitmap = decode(bitmapOptions, getNext());
            bitmapOptions.InBitmap = bitmap;
        

        private BitmapFactory.Options newOptions()
        
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.InSampleSize = 1;
            options.InMutable = true;
            options.InJustDecodeBounds = true;
            options.InPurgeable = true;
            options.InInputShareable = true;
            options.InPreferredConfig = Bitmap.Config.Rgb565;
            return options;
        

        private Bitmap decode(BitmapFactory.Options options, int imageRes)
        
            return BitmapFactory.DecodeResource(imageview.Resources, imageRes, bitmapOptions);
        

        public void SetOneShot(bool oneShot)
        
            this.oneShot = oneShot;
        

        private int getNext()
        
            currentFrame++;
            if (currentFrame >= animationFrames.Length)
            
                if (oneShot)
                
                    shouldRun = false;
                    currentFrame = animationFrames.Length - 1;
                
                else
                
                    currentFrame = 0;
                
            
            return animationFrames[currentFrame];
        

        public void stop()
        
            lock (Lock)
            
                shouldRun = false;
            
        

        public void start()
        
            lock (Lock)
            
                shouldRun = true;

                if (isRunning)
                
                    return;
                

                Action tempAction = new Action(delegate
                
                    if (!shouldRun || imageview == null)
                    
                        isRunning = false;
                        if (onAnimationStoppedListener != null)
                        
                            onAnimationStoppedListener.AnimationStopped();
                            onAnimationStoppedListener = null;
                            handler.RemoveCallbacks(action);
                        
                        return;
                    

                    isRunning = true;

                    handler.PostDelayed(action, delayMillis);

                    if (imageview.IsShown)
                    
                        int imageRes = getNext();
                        if (bitmap != null)
                        
                            if (Build.VERSION.SdkInt >= BuildVersionCodes.Honeycomb)
                            
                                if (bitmap != null && !bitmap.IsRecycled)
                                
                                    bitmap.Recycle();
                                    bitmap = null;
                                
                            

                            try
                            
                                bitmap = BitmapFactory.DecodeResource(imageview.Resources, imageRes, bitmapOptions);
                            
                            catch (Exception e)
                            
                                bitmap.Recycle();
                                bitmap = null;
                                Console.WriteLine("Exception: " + e.StackTrace);
                            

                            if (bitmap != null)
                            
                                imageview.SetImageBitmap(bitmap);
                            
                            else
                            
                                imageview.SetImageResource(imageRes);
                                bitmap.Recycle();
                                bitmap = null;
                            
                        
                        else
                        
                            imageview.SetImageResource(imageRes);
                        
                    
                );

                action = tempAction;

                handler.Post(action);
            
        
    

这是我的闪屏类:(该类从名为“splash_0001,splash_0002 ...”的可绘制文件夹中读取图像。因此无需在数组上命名图像资源。增加每帧的帧数秒 (FPS) 以加快动画速度)。

using Android.App;
using Android.Content;
using Android.OS;
using Android.Widget;
using ...Droid.Base;
using ...Droid.Util;
using System;
using System.Collections.Generic;
using static ...Util.FramesSequenceAnimation;

namespace ...Droid.Activities

    [Activity(MainLauncher = true)]
    public class SplashActivity : BaseActivity, FramesSequenceAnimationListener
    
        private FramesSequenceAnimation framesSequenceAnimation;

        private const string
            IMAGE_NAME_PREFIX = "splash_",
            KEY_CURRENT_FRAME = "key_current_frame";

        private int FPS = 50;

        private int numberOfImages;

        protected override OrientationEnum GetOrientation()
        
            return OrientationEnum.ORIENTATION_CHECK_DEVICE_SIZE;
        

        protected override void OnCreate(Bundle savedInstanceState)
        
            base.OnCreate(savedInstanceState);
            SetContentView(Resource.Layout.activity_splash);

            RelativeLayout background = FindViewById<RelativeLayout>(Resource.Id.splash_background);
            background.Click += Click;

            ImageView imageView = FindViewById<ImageView>(Resource.Id.splash_imageview);
            imageView.Click += Click;

            numberOfImages = GetSplashImagesCount();

            framesSequenceAnimation = new FramesSequenceAnimation(this, imageView, GetImageResourcesIDs(), FPS);
            framesSequenceAnimation.SetOneShot(true);

            if (savedInstanceState != null)
            
                int currentFrame = savedInstanceState.GetInt(KEY_CURRENT_FRAME) + 1;
                if (currentFrame < numberOfImages)
                
                    framesSequenceAnimation.SetCurrentFrame(currentFrame);
                
            

            framesSequenceAnimation.start();
        

        private int[] GetImageResourcesIDs()
        
            List<int> list = new List<int>();

            for (int i = 1; i <= numberOfImages; i++)
            
                var image_name = IMAGE_NAME_PREFIX + i.ToString().PadLeft(4, '0');
                int resID = Resources.GetIdentifier(image_name, "drawable", PackageName);
                list.Add(resID);
            

            return list.ToArray();
        

        private int GetSplashImagesCount()
        
            // Count number of images in drawable folder
            int count = 0;
            var fields = typeof(Resource.Drawable).GetFields();
            foreach (var field in fields)
            
                if (field.Name.StartsWith(IMAGE_NAME_PREFIX))
                
                    count++;
                
            

            return count;
        

        private void Click(object sender, EventArgs e)
        
            framesSequenceAnimation.SetFramesSequenceAnimationListener(null);
            GoToLoginScreen();
        

        private void GoToLoginScreen()
        
            Finish();
            StartActivity(new Intent(this, typeof(LoginActivity)));
            OverridePendingTransition(0, Resource.Animation.abc_fade_out);
        

        void FramesSequenceAnimationListener.AnimationStopped()
        
            GoToLoginScreen();
        

        protected override void OnSaveInstanceState(Bundle outState)
        
            base.OnSaveInstanceState(outState);

            outState.PutInt(KEY_CURRENT_FRAME, framesSequenceAnimation.GetCurrentFrame());
        
    

【讨论】:

你今天帮我节省了时间,效果很好+1

以上是关于在Android中的逐帧动画中导致OutOfMemoryError的主要内容,如果未能解决你的问题,请参考以下文章

使用画布的 HTML5 中的逐帧动画

Android逐帧动画和补间动画

android中的动画有哪几类

jQuery .hover() 或 .animate() 在 Marquee 插件中导致冒泡动画

Android动画使用总结

Android动画效果之Frame Animation(逐帧动画)