web-dev-qa-db-ja.com

Androidビットマップ制限-Java.lang.OutOfMemoryの防止

私は現在、Androidプラットフォームの奇妙な動作-ビットマップ/ Javaヒープメモリ制限に苦しんでいます。デバイスに応じて、Androidは、アプリ開発者をJavaヒープスペースの16、24、または32 MiBに制限します(または、ルート化された電話でランダムな値が見つかる場合があります)。これは間違いなく非常に小さいですが、次のAPIで使用量を測定できるため、比較的簡単です。

Runtime rt = Runtime.getRuntime();
long javaBytes = rt.totalMemory() - rt.freeMemory();
long javaLimit = rt.maxMemory();

簡単です。今ひねりのために。 Androidでは、ビットマップは、いくつかの例外を除いて、ネイティブヒープに格納され、Javaヒープにはカウントされません。グーグルの一部の明るい目で純粋な開発者は、これは「悪い」と判断し、開発者が「公正なシェア以上」を獲得できるようにしました。したがって、ビットマップや、場合によっては他のリソースによって発生するネイティブメモリ使用量を計算し、それをJavaヒープと合計し、それを超えると..... Java.langを計算するこの素敵な小さなコードがあります。 .OutOfMemory。 痛い

しかし、大したことはありません。私はたくさんのビットマップを持っています、そしてそれらのすべてをいつも必要とするわけではありません。現在使用されていないもののいくつかを「ページアウト」することができます。

そのため、試行#1では、コードをリファクタリングして、すべてのビットマップロードをtry/catchでラップできるようにしました。

while(true) {
    try {
        return BitmapFactory.decodeResource(context.getResources(), Android_id, bitmapFactoryOptions);
    } catch (Java.lang.OutOfMemory e) {
        // Do some logging

        // Now free some space (the code below is a simplified version of the real thing)
        Bitmap victim = selectVictim();
        victim.recycle();
        System.gc(); // REQUIRED; else, weird behavior ensues
    }
}

ほら、これが私のコードが例外をキャッチし、いくつかのビットマップをリサイクルしていることを示す素敵な小さなログスニペットです:

 E/Epic(23221):OUT_OF_MEMORY(Java.lang.OutOfMemoryをキャッチ)
 I/Epic(23221):ArchPlatform [Android] .logStats()-
 I/Epic (23221):LoadedClassCount = 0.00M 
 I/Epic(23221):GlobalAllocSize = 0.00M 
 I/Epic(23221):GlobalFreedSize = 0.02M 
 I/Epic(23221) ):GlobalExternalAllocSize = 0.00M 
 I/Epic(23221):GlobalExternalFreedSize = 0.00M 
 I/Epic(23221):EpicPixels = 26.6M(これはロードされたすべてのビットマップで4 * #pixels) 
 I/Epic(23221):NativeHeapSize = 29.4M 
 I/Epic(23221):NativeHeapAllocSize = 25.2M 
 I/Epic(23221):ThreadAllocSize = 0.00M 
 I/Epic(23221):totalMemory()= 9.1M 
 I/Epic(23221):maxMemory()= 32.0M 
 I/Epic(23221):freeMemory()= 4.4M 
 W/Epic(23221):ビットマップのリサイクル 'game_Word_puzzle_11_aniframe_005' 
 I/Epic(23221):BITMAP_RECYCLING:1.1Mに相当する1ビットマップのリサイクル)。年齢= 294 

TotalMemory-freeMemoryはわずか4.7MiBですが、〜26であることに注意してください。ビットマップによって占有されるネイティブメモリのMiBは、制限に達した31/32MiBの範囲にあります。ロードされたすべてのビットマップの実行中の集計は26.6MiBですが、ネイティブの割り当てサイズは25.2 MiBしかないため、ここではまだ少し混乱しています。だから私は何か間違ったことを数えています。しかし、それはすべて球場にあり、mem-limitで発生するクロスプールの「合計」を明確に示しています。

私はそれを修正したと思いました。しかし、いいえ、Androidはそれほど簡単に諦めません...

4つのテストデバイスのうち2つから得られるものは次のとおりです。

 I/dalvikvm-heap(17641):ターゲットGCヒープを32.687MBから32.000MBにクランプ
 D/dalvikvm(17641):GC_FOR_MALLOC解放<1K、41%解放4684K/7815K、外部24443K/24443K、一時停止24ms 
 D/dalvikvm(17641):GC_EXTERNAL_ALLOC解放<1K、41%解放4684K/7815K、外部24443K/24443K、一時停止29ms 
 E/dalvikvm-heap(17641): 1111200バイトの外部割り当てがこのプロセスには大きすぎます。
 E/dalvikvm(17641):メモリ不足:ヒープサイズ= 7815KB、割り当て済み= 4684KB、ビットマップサイズ= 24443KB、制限= 32768KB 
 E/dalvikvm(17641):トリム情報:Footprint = 7815KB、Allowed Footprint = 7815KB、Trimmed = 880KB 
 E/GraphicsJNI(17641):VMでは1111200バイトを割り当てられません
 I/dalvikvm-heap(17641):ターゲットGCヒープを32.686MBから32.000MBにクランプ
 D/dalvikvm(17641):GC_FOR_MALLOC解放<1K、41%解放4684K/7815K、外部24443K/24443K 、一時停止17ms 
 I/DEBUG(1505):*** *** *** *** *** *** *** *** *** *** *** ** * *** *** *** *** 
 I/DEBUG(1505):ビルドフィンガープリント: 'ver izon_wwe/htc_mecha/mecha:2.3.4/GRJ22/98797:user/release-keys '
 I/DEBUG(1505):pid:17641、tid:17641 
 I/DEBUG(1505) :信号11(SIGSEGV)、コード1(SEGV_MAPERR)、障害アドレス00000000 
 I/DEBUG(1505):r0 0055dab8 r1 00000000 r2 00000000 r3 0055dadc 
 I/DEBUG(1505):r4 0055dab8 r5 00000000 r6 00000000 r7 00000000 
 I/DEBUG(1505):r8 000002b7 r9 00000000 10 00000000 fp 00000384 
 I/DEBUG(1505):ip 0055dab8 sp befdb0c0 lr 00000000 pc ab14f11c cpsr 6000 ____。] I/DEBUG(1505):d0 414000003f800000 d1 2073646565637834 
 I/DEBUG(1505):d2 4de4b8bc426fb934 d3 42c80000007a1f34 
 I/DEBUG(1505):d4 00000008_ ] I/DEBUG(1505):d6 0000000000000000 d7 4080000080000000 
 I/DEBUG(1505):d8 0000025843e7c000 d9 c0c0000040c00000 
 I/DEBUG(1505):d10 40c0000040c00000 d11 0000000000000000 [.__/DEBUG(1505):d12 0000000000000000 d13 0000000000000000 
 I/DEBUG(1505):d14 0000000000000000 d15 0000000000000000 
 I/DEBUG(1505):d16 afd4242840704ab8 d17 0000000000000000 
 I/DEBUG(1505):d18 0000000000000000 d19 000000 .____。] I/DEBUG(1505):d20 0000000000000000 d21 0000000000000000 
 I/DEBUG(1505):d22 0000000000000000 d23 0000000000000000 
 I/DEBUG(1505):d24 0000000000000000 d25 0000000000000000 
 I/DEBUG(1505):d26 0000000000000000 d27 0000000000000000 
 I/DEBUG(1505):d28 00ff00ff00ff00ff d29 00ff00ff00ff00ff 
 I/DEBUG(1505):d30 0000000000000000 d31 3fe55167807de02 I/DEBUG(1505):scr 68000012 

これはネイティブクラッシュです。セグメンテーション違反も少なくありません(sig11)。定義上、セグメンテーション違反は常にバグです。これは、GCやmem-limitチェックを処理するネイティブコードのAndroidバグです。しかし、それでもクラッシュして悪いレビュー、返品、売り上げの減少をもたらすのは私のアプリです。

だから私は自分で限界を計算しなければなりません。 私がここで苦労したことを除いて。自分でピクセルを足し合わせてみましたが(EpicPixels)、それでも定期的にmemcrashにヒットしているので、何かを過小評価しています。 javaBytes(total-free)をNativeHeapAllocSizeに追加しようとしましたが、これによりアプリが「拒食症」になり、パージするものがなくなるまでビットマップを解放および解放することがありました。

  1. メモリ制限を計算してJava.lang.OutOfMemoryをトリガーするために使用されるexact計算を知っている人はいますか?

  2. 他の誰かがこの問題にぶつかってそれを解決しましたか?知恵の真珠はありますか?

  3. どのGoogle社員がこの計画を夢見ていたので、私の人生の40時間以内を台無しにするために彼を殴ることができるのか誰か知っていますか? j/k

回答:制限はNativeHeapAllocSize <maxMemory();です。ただし、メモリの断片化により、Androidは実際の制限のかなり前にクラッシュします。したがって、実際の制限よりもいくらか小さい値に制限する必要があります。この「安全率」はアプリによって異なりますが、ほとんどの人にとっていくつかのMiBが機能するようです。 (私はこの振る舞いがどれほど壊れているかに驚かされたと言うことができます)

25
Dave Dopson

このスニペットを使用して、私のために働いた

/**
 * Checks if a bitmap with the specified size fits in memory
 * @param bmpwidth Bitmap width
 * @param bmpheight Bitmap height
 * @param bmpdensity Bitmap bpp (use 2 as default)
 * @return true if the bitmap fits in memory false otherwise
 */
public static boolean checkBitmapFitsInMemory(long bmpwidth,long bmpheight, int bmpdensity ){
    long reqsize=bmpwidth*bmpheight*bmpdensity;
    long allocNativeHeap = Debug.getNativeHeapAllocatedSize();


    final long heapPad=(long) Math.max(4*1024*1024,Runtime.getRuntime().maxMemory()*0.1);
    if ((reqsize + allocNativeHeap + heapPad) >= Runtime.getRuntime().maxMemory())
    {
        return false;
    }
    return true;

}

これが使用例です

        BitmapFactory.Options bmpFactoryOptions = new BitmapFactory.Options();
        bmpFactoryOptions.inJustDecodeBounds=true;
        BitmapFactory.decodeFile(path,bmpFactoryOptions);
        if ( (runInSafeMemoryMode()) && (!Manager.checkBitmapFitsInMemory(bmpFactoryOptions.outWidth, bmpFactoryOptions.outHeight, 2)) ){
            Log.w(TAG,"Aborting bitmap load for avoiding memory crash");
            return null;        
        }
18
Addev

制限はデバイスごとに異なります(ビットマップをそのままロードする場合は3番目のリンクを使用します)。または、次のような問題を回避するためのトリックがいくつかあります。

  • クラッシュを回避するために一部のメモリを解放するには、ApplicationクラスのonLowMemory()を使用します。
  • デコードする前に、ビットマップに必要なサイズを指定してください。詳細については、次のリンクを確認してください。

http://davidjhinson.wordpress.com/2010/05/19/scarce-commodities-google-Android-memory-and-bitmaps/

ビットマップオブジェクトに画像をロードする際のメモリ不足の問題

ヒープをチェックするためのこのリンクショー

BitmapFactory OOMが私を狂わせている

  • そしてもちろん、古いビットマップのメモリを解放します
4
Addev

さて、ネイティブモードでの制限が合計 Javaヒープサイズ+ネイティブ使用メモリ)に適用されているのではないかと思い始めています。

制限は、NativeHeapAllocSizeとmaxMemory()に基づいています。 22.0 MiB/24 MiBにいるときに、最大1MiBを割り当ててクラッシュしていることが以下に表示されます。制限は、割り当てることができるメモリの量の上限です。これは私をしばらくの間投げたものです。制限に達する前に、クラッシュが大幅に発生します。したがって、23.999 MiB/24 MiBを割り当てようとすると、ほぼ100%の確率でクラッシュが発生するため、ソリューションに「memoryPad」値が必要になります。では、制限が24 MiBの場合、どれだけ安全に使用できますか?わからない。 20MiBは機能しているようです。 22MiBは機能しているようです。それ以上に近づけるのは緊張します。量は、ネイティブプロセスでmallocメモリスペースがどの程度断片化されているかによって異なります。そしてもちろん、これを測定する方法はないので、安全側に誤りがあります。

 07-31 18:37:19.031:警告/エピック(3118):メモリ使用:27.3M = 4.2M + 23.0M。 jf = 1.7M、nhs = 23.3M、nhf = 0.0M 
 07-31 18:37:19.081:INFO/Epic(3118):ArchPlatform [Android] .logStats()-
 07 -31 18:37:19.081:INFO/Epic(3118):LoadedClassCount = 0.00M 
 07-31 18:37:19.081:INFO/Epic(3118):GlobalAllocSize = 0.02M 
 07 -31 18:37:19.081:INFO/Epic(3118):GlobalFreedSize = 0.05M 
 07-31 18:37:19.081:INFO/Epic(3118):GlobalExternalAllocSize = 0.00M 
 07 -31 18:37:19.081:INFO/Epic(3118):GlobalExternalFreedSize = 0.00M 
 07-31 18:37:19.081:INFO/Epic(3118):EpicPixels = 17.9M 
 07 -31 18:37:19.081:INFO/Epic(3118):NativeHeapSize = 22.2M 
 07-31 18:37:19.081:INFO/Epic(3118):NativeHeapFree = 0.07M 
 07 -31 18:37:19.081:INFO/Epic(3118):NativeHeapAllocSize = 22.0M 
 07-31 18:37:19.081:INFO/Epic(3118):ThreadAllocSize = 0.12M 
 07 -31 18:37:19.081:INFO/Epic(3118):totalMemory()= 5.7M 
 07-31 18:37:19.081:INFO/Epic(3118):maxMemory()= 24.0M 
 07-31 18:37:19.081:INFO/Epic(3118):freeMemory()= 1.6M 
 07-31 18:37:19.081:INF O/Epic(3118):app.mi.availMem = 126.5M 
 07-31 18:37:19.081:INFO/Epic(3118):app.mi.threshold = 16.0M 
 07 -31 18:37:19.081:INFO/Epic(3118):app.mi.lowMemory = false 
 07-31 18:37:19.081:INFO/Epic(3118):dbg.mi.dalvikPrivateDirty = 0.00 M 
 07-31 18:37:19.081:INFO/Epic(3118):dbg.mi.dalvikPss = 0.00M 
 07-31 18:37:19.081:INFO/Epic(3118) :dbg.mi.dalvikSharedDirty = 0.00M 
 07-31 18:37:19.081:INFO/Epic(3118):dbg.mi.nativePrivateDirty = 0.00M 
 07-31 18:37: 19.081:INFO/Epic(3118):dbg.mi.nativePss = 0.00M 
 07-31 18:37:19.081:INFO/Epic(3118):dbg.mi.nativeSharedDirty = 0.00M 
 07-31 18:37:19.081:INFO/Epic(3118):dbg.mi.otherPrivateDirty = 0.02M 
 07-31 18:37:19.081:INFO/Epic(3118):dbg.mi。 otherPss0.02M 
 07-31 18:37:19.081:INFO/Epic(3118):dbg.mi.otherSharedDirty = 0.00M 
 07-31 18:37:19.081:ERROR/dalvikvm- heap(3118):このプロセスには1111200バイトの外部割り当てが大きすぎます。
 07-31 18:37:19.081:エラー/ dalvikvm(3118):メモリ不足:ヒープサイズ= 6535KB、割り当て済み= 4247KB、ビットマップサイズ= 17767KB 
 07-31 18:37:19.081:エラー/グラフィックスJNI(3118):VM割り当てられません1111200バイト

それらすべてを印刷するためのコード:


    public static void logMemoryStats() {
        String text = "";
        text += "\nLoadedClassCount="               + toMib(Android.os.Debug.getLoadedClassCount());
        text += "\nGlobalAllocSize="                + toMib(Android.os.Debug.getGlobalAllocSize());
        text += "\nGlobalFreedSize="                + toMib(Android.os.Debug.getGlobalFreedSize());
        text += "\nGlobalExternalAllocSize="        + toMib(Android.os.Debug.getGlobalExternalAllocSize());
        text += "\nGlobalExternalFreedSize="        + toMib(Android.os.Debug.getGlobalExternalFreedSize());
        text += "\nEpicPixels="                     + toMib(EpicBitmap.getGlobalPixelCount()*4);
        text += "\nNativeHeapSize="                 + toMib(Android.os.Debug.getNativeHeapSize());
        text += "\nNativeHeapFree="                 + toMib(Android.os.Debug.getNativeHeapFreeSize());
        text += "\nNativeHeapAllocSize="            + toMib(Android.os.Debug.getNativeHeapAllocatedSize());
        text += "\nThreadAllocSize="                + toMib(Android.os.Debug.getThreadAllocSize());

        text += "\ntotalMemory()="                  + toMib(Runtime.getRuntime().totalMemory());
        text += "\nmaxMemory()="                    + toMib(Runtime.getRuntime().maxMemory());
        text += "\nfreeMemory()="                   + toMib(Runtime.getRuntime().freeMemory());

        Android.app.ActivityManager.MemoryInfo mi1 = new Android.app.ActivityManager.MemoryInfo();
        ActivityManager am = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE);
        am.getMemoryInfo(mi1);
        text += "\napp.mi.availMem="                + toMib(mi1.availMem);
        text += "\napp.mi.threshold="               + toMib(mi1.threshold);
        text += "\napp.mi.lowMemory="               + mi1.lowMemory;

        Android.os.Debug.MemoryInfo mi2 = new Android.os.Debug.MemoryInfo();        
        Debug.getMemoryInfo(mi2);
        text += "\ndbg.mi.dalvikPrivateDirty="      + toMib(mi2.dalvikPrivateDirty);
        text += "\ndbg.mi.dalvikPss="               + toMib(mi2.dalvikPss);
        text += "\ndbg.mi.dalvikSharedDirty="       + toMib(mi2.dalvikSharedDirty);
        text += "\ndbg.mi.nativePrivateDirty="      + toMib(mi2.nativePrivateDirty);
        text += "\ndbg.mi.nativePss="               + toMib(mi2.nativePss);
        text += "\ndbg.mi.nativeSharedDirty="       + toMib(mi2.nativeSharedDirty);
        text += "\ndbg.mi.otherPrivateDirty="       + toMib(mi2.otherPrivateDirty);
        text += "\ndbg.mi.otherPss"                 + toMib(mi2.otherPss);
        text += "\ndbg.mi.otherSharedDirty="        + toMib(mi2.otherSharedDirty);

        EpicLog.i("ArchPlatform[Android].logStats() - " + text);
    }
3
Dave Dopson