java.lang.OutOfMemoryError:ビットマップのサイズがVMの予算を超える-Android


159

Androidで画像をたくさん使うアプリを開発しました。

一度アプリを実行すると、画面上の情報(充填LayoutsListviewsTextviewsImageViews、など)を、ユーザが情報を読み取ります。

アニメーションや特殊効果など、記憶をいっぱいにするものはありません。ドローアブルは変更される場合があります。一部はAndroidリソースであり、一部はSDCARDのフォルダーに保存されたファイルです。

次に、ユーザーが終了し(onDestroyメソッドが実行され、アプリがVMによってメモリにとどまります)、その後、ある時点でユーザーが再び入力します。

ユーザーがアプリにアクセスするたびに、ユーザーがを取得するまで、メモリがどんどん増えているのがわかりますjava.lang.OutOfMemoryError

それで、多くの画像を処理するための最良の/正しい方法は何ですか?

常にロードされないように、静的メソッドに配置する必要がありますか?レイアウトまたはレイアウトで使用されている画像を特別な方法でクリーニングする必要がありますか?


4
これは、変更可能なドローアブルがたくさんある場合に役立ちます。私は自分自身:)プログラムでしたので、それは働いているandroidactivity.wordpress.com/2011/09/24/...

回答:


72

メモリリークがあるようです。問題は多くの画像を処理していないことです。それは、アクティビティが破棄されたときに画像が割り当て解除されないことです。

コードを見ないでこれがなぜであるかを言うのは難しいです。ただし、この記事には役立ついくつかのヒントがあります。

http://android-developers.blogspot.de/2009/01/avoiding-memory-leaks.html

特に、静的変数を使用すると、状況が悪化する可能性が高くなります。アプリケーションの再描画時にコールバックを削除するコードを追加する必要があるかもしれませんが、ここでも、確かに言うには十分な情報がありません。


8
これは真実であり、他の多くのアプリが実行されている、メモリの断片化などの複数の理由により、多くの画像(メモリリークではない)もoutOfMemoryを引き起こす可能性があります。多くの画像を操作する場合、outOfMemoryを取得すると体系的には、常に同じように、それがリークです。1日1回かそれと同じようにそれを取得する場合、それはあなたが限界に近すぎるためです。この場合の私の提案は、いくつかのものを削除して再試行することです。また、画像メモリはヒープメモリの外にあるため、両方に注意してください。
Daniel Benedykt、2010年

15
メモリリークが疑われる場合、ヒープの使用状況を監視する簡単な方法は、adb shell dumpsys meminfoコマンドを使用することです。ヒープの使用量が数gcサイクル(logcatのGC_ *ログ行)を超えて増加している場合は、リークが発生していると確信できます。次に、adbまたはDDMSを介してヒープダ​​ンプ(または異なるタイミングでいくつか)を作成し、Eclipse MATのドミネーターツリーツールを介してそれを分析します。時間の経過とともに大きくなる保持ヒープを持つオブジェクトをすぐに見つける必要があります。それはあなたのメモリリークです。私はここにいくつかの詳細と記事を書いた:macgyverdev.blogspot.com/2011/11/...
ヨハン・ノレン

98

私がAndroidアプリの開発で見つけた最も一般的なエラーの1つは、「java.lang.OutOfMemoryError:Bitmap Size Exceeds VM Budget」エラーです。方向を変更した後、多くのビットマップを使用するアクティビティでこのエラーが頻繁に見つかりました。アクティビティが破棄され、再作成され、ビットマップに使用できるVMメモリを消費するXMLからレイアウトが「膨張」します。

以前のアクティビティレイアウトのビットマップは、アクティビティへの参照をクロスしているため、ガベージコレクターによって適切に割り当て解除されません。多くの実験の後、私はこの問題の非常に良い解決策を見つけました。

まず、XMLレイアウトの親ビューに「id」属性を設定します。

    <?xml version="1.0" encoding="utf-8"?>
    <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="fill_parent"
     android:layout_height="fill_parent"
     android:id="@+id/RootView"
     >
     ...

次に、onDestroy() アクティビティのメソッドでunbindDrawables()、親ビューへの参照を渡してメソッドを呼び出し、次にを実行しSystem.gc()ます。

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

    unbindDrawables(findViewById(R.id.RootView));
    System.gc();
    }

    private void unbindDrawables(View view) {
        if (view.getBackground() != null) {
        view.getBackground().setCallback(null);
        }
        if (view instanceof ViewGroup) {
            for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
            unbindDrawables(((ViewGroup) view).getChildAt(i));
            }
        ((ViewGroup) view).removeAllViews();
        }
    }

このunbindDrawables()メソッドは、ビューツリーを再帰的に探索します。

  1. すべての背景ドローアブルのコールバックを削除します
  2. すべてのビューグループの子を削除します

10
例外... java.lang.UnsupportedOperationException:removeAllViews()はAdapterViewではサポートされていません

おかげで、私はたくさんのドローアブルを持っているので、ヒープサイズを大幅に減らすことができます... @GJTorikianそのため、ほとんどすべてのViewGroupサブクラスが問題なく動作するremoveAllViews()でキャッチしてみてください。
Eric Chen

5
または単に条件を変更します:if(view instanceof ViewGroup &&!(view instanceof AdapterView))
Anke

2
@Adam Varhegyi、onDestroyのギャラリービューで同じ関数を明示的に呼び出すことができます。unbindDrawables(galleryView);のように。これがうまくいくことを願っています...
hp.android

1
これら二つの落とし穴のために気をつけstackoverflow.com/questions/8405475/...stackoverflow.com/questions/4486034/...
max4ever

11

この問題を回避するには、ネイティブメソッドを使用することができますBitmap.recycle()前に、null-ing Bitmapオブジェクトを(または別の値を設定します)。例:

public final void setMyBitmap(Bitmap bitmap) {
  if (this.myBitmap != null) {
    this.myBitmap.recycle();
  }
  this.myBitmap = bitmap;
}

次に、次のようにmyBitmap呼び出しなしで変更できますSystem.gc()

setMyBitmap(null);    
setMyBitmap(anotherBitmap);

1
これらの要素をリストビューに追加しようとすると、これは機能しません。それについて何か提案はありますか?
ラファエルサンチェス

@ddmytrenko割り当てる前にイメージをリサイクルしています。ピクセルがレンダリングされないのではないでしょうか?
IgorGanapolsky 2013年

8

私はこの正確な問題に遭遇しました。ヒープはかなり小さいので、これらのイメージはメモリに関してかなり早く制御不能になる可能性があります。1つの方法は、ガベージコレクターにリサイクルメソッドを呼び出しビットマップ上のメモリを収集するためのヒントを与えることです。

また、onDestroyメソッドが呼び出されることは保証されていません。このロジック/クリーンアップをonPauseアクティビティに移動することができます。詳細については、このページのアクティビティライフサイクル図/表をご覧ください。


情報をありがとう。私はビットマップではなくすべてのドローアブルを管理しているので、リサイクルを呼び出すことができません。ドローアブルに関する他の提案はありますか?おかげでダニエル
ダニエルベネディクト

onPause提案をありがとう。私はBitmapすべてに画像が含まれている4つのタブを使用しているため、ユーザーがすべてのタブを参照した場合、アクティビティの破棄はすぐには発生しない可能性があります。
Azurespot 2015年

7

この説明は役立つかもしれません:http : //code.google.com/p/android/issues/detail?id=8488#c80

「速いヒント:

1)System.gc()を自分で呼び出さないでください。これは、ここでの修正として伝搬され、機能しません。それをしません。私の説明で気付いた場合、OutOfMemoryErrorが発生する前に、JVMはすでにガベージコレクションを実行しているため、再度実行する理由はありません(プログラムの速度が低下します)。アクティビティの最後に1つ実行すると、問題が隠されます。これにより、ビットマップがファイナライザキューに速く配置される可能性がありますが、代わりに各ビットマップで単にリサイクルを呼び出すことができなかったわけではありません。

2)不要になったビットマップでは常にrecycle()を呼び出します。少なくとも、アクティビティのonDestroyで、使用していたすべてのビットマップを調べてリサイクルします。また、ビットマップインスタンスをdalvikヒープからより速く収集したい場合でも、ビットマップへの参照をクリアしても問題はありません。

3)recycle()を呼び出してからSystem.gc()を呼び出しても、Dalvikヒープからビットマップが削除されない場合があります。これについて気にしないでください。recycle()がその仕事をしてネイティブメモリを解放したので、Dalvikヒープからビットマップを実際に削除するには、前に概説した手順を実行するのに少し時間がかかります。ネイティブメモリの大きなチャンクはすでに解放されているため、これは大した問題ではありません。

4)フレームワークに最後にバグがあると常に想定します。Dalvikは本来の役割を果たしています。それはあなたが期待するものやあなたが望むものではないかもしれませんが、それがどのように機能するかです。」


5

私はまったく同じ問題を抱えていました。数回のテストの結果、このエラーは大きな画像スケーリングで発生していることがわかりました。画像のスケーリングを減らしたところ、問題は解消しました。

PS最初は、画像を縮小せずに画像サイズを小さくしようとしました。それはエラーを止めませんでした。


4
あなたが画像スケーリングをどのように行ったかについてコードを投稿してもらえますか?私は同じ問題に直面しており、これで解決できると思います。ありがとう!
グリーガー、2011年

@Gix-彼は、画像をプロジェクトリソースに取り込む前に画像のサイズを小さくする必要があったため、ドローアブルのメモリフットプリントを低くする必要があることを意味します。このためには、お好みのフォトエディタを使用する必要があります。私はpixlr.comが好きです
カイルクレッグ

5

以下のポイントは本当に私を助けてくれました。他のポイントもあるかもしれませんが、これらは非常に重要です:

  1. 可能な限り、(activity.thisではなく)アプリケーションコンテキストを使用します。
  2. アクティビティのonPause()メソッドでスレッドを停止して解放します
  3. アクティビティのonDestroy()メソッドでビュー/コールバックを解放します

4

この問題を解決する便利な方法を提案します。エラーが発生したアクティビティのMainfest.xmlで、属性「android:configChanges」の値を割り当てるだけです。このような:

<activity android:name=".main.MainActivity"
              android:label="mainActivity"
              android:configChanges="orientation|keyboardHidden|navigation">
</activity>

私が最初に出した解決策は、OOMエラーの頻度を実際に低いレベルに減らしました。しかし、それは問題を完全に解決しませんでした。そして、私は2番目の解決策を提供します:

OOMが詳述しているように、ランタイムメモリを使いすぎました。そこで、プロジェクトの〜/ res / drawableで画像サイズを縮小します。128X128の解像度を持つオーバークオリファイド画像などは、私のアプリケーションにも適した64x64にサイズ変更できます。そして、写真の山でそうした後、OOMエラーは再び発生しません。


1
これは、アプリの再起動を回避しているため機能します。したがって、それは回避策であるので、それほど多くの解決策ではありません。しかし、時にはそれだけで十分です。また、ActivityのデフォルトのonConfigurationChanged()メソッドは方向を反転するため、さまざまな方向に適応するためにUIを変更する必要がない場合は、結果に完全に満足できる可能性があります。ただし、再起動する可能性のある他のことに注意してください。これを使用することができます:android:configChanges = "keyboardHidden | orientation | keyboard | locale | mcc | mnc | touchscreen | screenLayout | fontScale"
カール

3

私もメモリ不足のバグに不満を感じています。そして、はい、私もこの画像を拡大縮小するときにこのエラーが頻繁にポップアップすることを発見しました。最初はすべての密度の画像サイズを作成してみましたが、これによりアプリのサイズが大幅に増加することがわかりました。そのため、すべての密度に1つの画像を使用し、画像をスケーリングしています。

ユーザーが1つのアクティビティから別のアクティビティに移動するたびに、アプリケーションでメモリ不足エラーがスローされました。ドローアブルをnullに設定してSystem.gc()を呼び出しても機能せず、getBitMap()。recycle()でビットマップドローアブルをリサイクルすることもできませんでした。Androidは、最初のアプローチでは引き続きメモリ不足エラーをスローし、2番目のアプローチでリサイクルされたビットマップを使用しようとすると、キャンバスエラーメッセージがスローされます。

私はさらに3番目のアプローチを採用しました。すべてのビューをnullに設定し、背景を黒に設定します。このクリーンアップはonStop()メソッドで行います。これは、アクティビティが表示されなくなるとすぐに呼び出されるメソッドです。onPause()メソッドでこれを行うと、ユーザーには黒い背景が表示されます。理想的ではありません。onDestroy()メソッドでそれを行うことに関しては、それが呼び出されるという保証はありません。

ユーザーがデバイスの戻るボタンを押したときに黒い画面が発生しないようにするには、startRetivity()メソッドでアクティビティをリロードします。

注:背景を黒に変更する必要はありません。


1

大きなビットマップを効率的にロードするレッスンで説明したBitmapFactory.decode *メソッドは、ソースデータがディスクまたはネットワークの場所(または実際にはメモリ以外のソース)から読み取られる場合、メインUIスレッドで実行しないでください。このデータの読み込みにかかる時間は予測不可能であり、さまざまな要因(ディスクまたはネットワークからの読み取り速度、イメージのサイズ、CPUの能力など)に依存します。これらのタスクの1つがUIスレッドをブロックする場合、システムはアプリケーションに非応答のフラグを立て、ユーザーはそれを閉じるオプションを選択できます(詳細については、応答性の設計を参照してください)。


0

まあ、インターネットで見つけたものはすべて試しましたが、どれもうまくいきませんでした。System.gc()を呼び出すと、アプリの速度が低下するだけです。onDestroyでのビットマップのリサイクルは、私にとってもうまくいきませんでした。

現在機能しているのは、すべてのビットマップの静的リストを作成して、再起動後もビットマップが存続するようにすることです。そして、再起動された場合、毎回新しいビットマップを作成する代わりに、保存されたビットマップを使用するだけです。

私の場合、コードは次のようになります。

private static BitmapDrawable currentBGDrawable;

if (new File(uriString).exists()) {
    if (!uriString.equals(currentBGUri)) {
        freeBackground();
        bg = BitmapFactory.decodeFile(uriString);

        currentBGUri = uriString;
        bgDrawable = new BitmapDrawable(bg);
        currentBGDrawable = bgDrawable;
    } else {
        bgDrawable = currentBGDrawable;
    }
}

これはあなたの文脈を漏らさないでしょうか?(活動データ)
ラファエルサンチェス

0

適切なサイズの背景画像を切り替えるだけでも同じ問題が発生しました。新しい画像を入力する前に、ImageViewをnullに設定した方が良い結果が得られました。

ImageView ivBg = (ImageView) findViewById(R.id.main_backgroundImage);
ivBg.setImageDrawable(null);
ivBg.setImageDrawable(getResources().getDrawable(R.drawable.new_picture));

0

FWIW、これが私がコーディングして数か月間使用した軽量のビットマップキャッシュです。それはすべての口笛ではないので、使用する前にコードを読んでください。

/**
 * Lightweight cache for Bitmap objects. 
 * 
 * There is no thread-safety built into this class. 
 * 
 * Note: you may wish to create bitmaps using the application-context, rather than the activity-context. 
 * I believe the activity-context has a reference to the Activity object. 
 * So for as long as the bitmap exists, it will have an indirect link to the activity, 
 * and prevent the garbaage collector from disposing the activity object, leading to memory leaks. 
 */
public class BitmapCache { 

    private Hashtable<String,ArrayList<Bitmap>> hashtable = new Hashtable<String, ArrayList<Bitmap>>();  

    private StringBuilder sb = new StringBuilder(); 

    public BitmapCache() { 
    } 

    /**
     * A Bitmap with the given width and height will be returned. 
     * It is removed from the cache. 
     * 
     * An attempt is made to return the correct config, but for unusual configs (as at 30may13) this might not happen.  
     * 
     * Note that thread-safety is the caller's responsibility. 
     */
    public Bitmap get(int width, int height, Bitmap.Config config) { 
        String key = getKey(width, height, config); 
        ArrayList<Bitmap> list = getList(key); 
        int listSize = list.size();
        if (listSize>0) { 
            return list.remove(listSize-1); 
        } else { 
            try { 
                return Bitmap.createBitmap(width, height, config);
            } catch (RuntimeException e) { 
                // TODO: Test appendHockeyApp() works. 
                App.appendHockeyApp("BitmapCache has "+hashtable.size()+":"+listSize+" request "+width+"x"+height); 
                throw e ; 
            }
        }
    }

    /**
     * Puts a Bitmap object into the cache. 
     * 
     * Note that thread-safety is the caller's responsibility. 
     */
    public void put(Bitmap bitmap) { 
        if (bitmap==null) return ; 
        String key = getKey(bitmap); 
        ArrayList<Bitmap> list = getList(key); 
        list.add(bitmap); 
    }

    private ArrayList<Bitmap> getList(String key) {
        ArrayList<Bitmap> list = hashtable.get(key);
        if (list==null) { 
            list = new ArrayList<Bitmap>(); 
            hashtable.put(key, list); 
        }
        return list;
    } 

    private String getKey(Bitmap bitmap) {
        int width = bitmap.getWidth();
        int height = bitmap.getHeight();
        Config config = bitmap.getConfig();
        return getKey(width, height, config);
    }

    private String getKey(int width, int height, Config config) {
        sb.setLength(0); 
        sb.append(width); 
        sb.append("x"); 
        sb.append(height); 
        sb.append(" "); 
        switch (config) {
        case ALPHA_8:
            sb.append("ALPHA_8"); 
            break;
        case ARGB_4444:
            sb.append("ARGB_4444"); 
            break;
        case ARGB_8888:
            sb.append("ARGB_8888"); 
            break;
        case RGB_565:
            sb.append("RGB_565"); 
            break;
        default:
            sb.append("unknown"); 
            break; 
        }
        return sb.toString();
    }

}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.