160

Androidで画像を多用するアプリを開発しました。

アプリが 1 回実行され、画面に情報が入力され ( LayoutsListviewsTextviewsImageViewsなど)、ユーザーが情報を読み取ります。

アニメーションも、特殊効果も、記憶を埋め尽くすものもありません。場合によっては、ドローアブルが変更されることがあります。一部は Android リソースであり、一部は SDCARD 内のフォルダーに保存されたファイルです。

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

ユーザーがアプリに入るたびに、ユーザーがjava.lang.OutOfMemoryError.

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

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

4

13 に答える 13

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. すべてのビューグループで子を削除します
于 2011-07-21T15:57:53.980 に答える
74

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

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

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

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

于 2009-12-22T21:55:12.380 に答える
7

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

"簡単なヒント:

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

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

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

4) 最後に、フレームワークにバグがあると常に想定します。Dalvik は、本来あるべきことを正確に行っています。それはあなたが期待したり望んでいるものではないかもしれませんが、それがどのように機能するかです. "

于 2012-04-11T00:52:10.570 に答える
5

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

  1. 可能な限り、(activity.thisの代わりに)アプリケーションコンテキストを使用してください。
  2. onPause()アクティビティメソッドでスレッドを停止して解放します
  3. onDestroy()メソッドのアクティビティでビュー/コールバックを解放します
于 2012-05-07T15:12:55.717 に答える
5

私はまったく同じ問題を抱えていました。いくつかのテストの後、このエラーは大きな画像のスケーリングで表示されることがわかりました。画像のスケーリングを減らしたところ、問題はなくなりました。

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

于 2010-12-20T07:56:19.253 に答える
3

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

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

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

ユーザーがデバイスの [戻る] ボタンを押したときに黒い画面が表示されないようにするために、onRestart() メソッドで startActivity(getIntent()) メソッドを呼び出してから、finish() メソッドを呼び出してアクティビティを再読み込みします。

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

于 2012-04-23T21:43:34.370 に答える
1

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

于 2012-05-07T13:06:56.100 に答える
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;
    }
}
于 2012-04-18T06:40:34.653 に答える
0

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

ImageView ivBg = (ImageView) findViewById(R.id.main_backgroundImage);
ivBg.setImageDrawable(null);
ivBg.setImageDrawable(getResources().getDrawable(R.drawable.new_picture));
于 2013-05-20T23:22:45.887 に答える
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();
    }

}
于 2013-11-10T04:51:06.910 に答える