Android Bitmap Limit - Предотвращение java.lang.OutOfMemory

В настоящее время я борюсь с нечетным поведением платформы Android - предел памяти кучи битмапа /Java. В зависимости от устройства Android ограничивает разработчика приложений до 16, 24 или 32 мегабайт пространства кучи Java (или вы можете найти произвольное значение на корневом телефоне). Это, возможно, довольно мало, но относительно просто, поскольку я могу измерить использование со следующими API:

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

достаточно просто; теперь для завихрения. В Android растровые изображения, за небольшим исключением, хранятся в нативной куче и не учитываются в куче Java. Некоторые яркие, пуристские разработчики в Google решили, что это "плохо", и позволил разработчику получить "больше, чем их справедливая доля". Таким образом, есть эта небольшая часть кода, которая вычисляет использование собственной памяти, используемую растровыми изображениями и, возможно, другими ресурсами, и суммы, которые с кучей 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 (caught 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 (this is 4 * #pixels in all loaded bitmaps)
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): Recycling bitmap 'game_word_puzzle_11_aniframe_005'
I/Epic    (23221): BITMAP_RECYCLING: recycled 1 bitmaps worth 1.1M).  age=294

Обратите внимание, как totalMemory - freeMemory всего 4,7 MiB, но с ~ 26? MiB собственной памяти, занятой растровыми изображениями, мы находимся в диапазоне 31/32 MiB, где мы достигли предела. Я все еще немного запутался здесь, так как мой текущий подсчет всех загруженных растровых изображений - 26,6 MiB, но собственный размер дистрибутива - всего 25,2 MiB. Поэтому я считаю что-то не так. Но все это на футбольном поле и определенно демонстрирует "суммирование" кросс-пула, происходящее с мем-лимитом.

Я ХОЧУ У меня это исправлено. Но нет, Android не будет так легко сдаваться...

Вот что я получаю от двух моих четырех тестовых устройств:

I/dalvikvm-heap(17641): Clamp target GC heap from 32.687MB to 32.000MB
D/dalvikvm(17641): GC_FOR_MALLOC freed <1K, 41% free 4684K/7815K, external 24443K/24443K, paused 24ms
D/dalvikvm(17641): GC_EXTERNAL_ALLOC freed <1K, 41% free 4684K/7815K, external 24443K/24443K, paused 29ms
E/dalvikvm-heap(17641): 1111200-byte external allocation too large for this process.
E/dalvikvm(17641): Out of memory: Heap Size=7815KB, Allocated=4684KB, Bitmap Size=24443KB, Limit=32768KB
E/dalvikvm(17641): Trim info: Footprint=7815KB, Allowed Footprint=7815KB, Trimmed=880KB
E/GraphicsJNI(17641): VM won't let us allocate 1111200 bytes
I/dalvikvm-heap(17641): Clamp target GC heap from 32.686MB to 32.000MB
D/dalvikvm(17641): GC_FOR_MALLOC freed <1K, 41% free 4684K/7815K, external 24443K/24443K, paused 17ms
I/DEBUG   ( 1505): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
I/DEBUG   ( 1505): Build fingerprint: 'verizon_wwe/htc_mecha/mecha:2.3.4/GRJ22/98797:user/release-keys'
I/DEBUG   ( 1505): pid: 17641, tid: 17641
I/DEBUG   ( 1505): signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 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 60000010
I/DEBUG   ( 1505):  d0  414000003f800000  d1  2073646565637834
I/DEBUG   ( 1505):  d2  4de4b8bc426fb934  d3  42c80000007a1f34
I/DEBUG   ( 1505):  d4  00000008004930e0  d5  0000000000000000
I/DEBUG   ( 1505):  d6  0000000000000000  d7  4080000080000000
I/DEBUG   ( 1505):  d8  0000025843e7c000  d9  c0c0000040c00000
I/DEBUG   ( 1505):  d10 40c0000040c00000  d11 0000000000000000
I/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 0000000000000000
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 3fe55167807de022
I/DEBUG   ( 1505):  scr 68000012

Это родной сбой. Сегфакт не менее (sig11). По определению, segfault ВСЕГДА появляется ошибка. Это абсолютно ошибка Android в собственной обработке кода GC и/или проверке mem-limit. Но это все еще мое приложение, которое разбивается, что приводит к плохим отзывам, возвращает и снижает продажи.

Поэтому я должен сам вычислить предел. За исключением того, что я боролся здесь. Я попытался скомпоновать пиксели (EpicPixels), но я все равно попадал в memcrash, поэтому я что-то недооцениваю. Я попробовал добавить javaBytes (total-free) в NativeHeapAllocSize, но это иногда приводило к тому, что мое приложение становилось "анорексическим", освобождая и освобождая растровые изображения, пока не было ничего, что можно было бы очистить.

  • Кто-нибудь знает точное вычисление, используемое для вычисления предела памяти и запускает java.lang.OutOfMemory?

  • Кто-нибудь еще ударил эту проблему и работал через нее? У вас есть жемчужины мудрости?

  • Кто-нибудь знает, какой сотрудник Google придумал эту схему, чтобы я мог ударить его за то, что он разрушил 40 часов моей жизни? J/кудаp >

ANSWER: предел для NativeHeapAllocSize < maxMemory(); однако из-за фрагментации памяти Android падает до фактического предела. Таким образом, вы должны ограничить себя значением, несколько меньшим фактического предела. Этот "фактор безопасности" зависит от приложения, но несколько MiB, похоже, работают для большинства людей. (могу ли я просто сказать, что я взорван тем, насколько нарушен это поведение)

Ответ 1

Используйте этот snipplet, работал у меня

/**
 * 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;        
        }

Ответ 2

Предел варьируется в зависимости от каждого устройства (используйте третью ссылку, если вы хотите загрузить растровое изображение как есть), или здесь у вас есть некоторые трюки, чтобы избежать такой проблемы, как:

  • используйте класс onLowMemory() класса Application для освобождения памяти, избегающей сбоя.
  • Перед декодированием укажите желаемый размер растрового изображения. Проверьте, что ссылки для получения дополнительной информации:

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

Странная проблема с памятью при загрузке изображения в объект Bitmap

Эта ссылка показывает, чтобы проверить кучу

BitmapFactory OOM заставляет меня орехи

  • И, конечно, освободите память старых растровых изображений

Ответ 3

ОК, поэтому я начинаю подозревать, что предел в основном режиме принудительно применяется для общего размера кучи java + собственной используемой памяти.

Предел основан на NativeHeapAllocSize против maxMemory(). Ниже вы увидите, что я разбиваю выделение ~ 1 MiB, пока я нахожусь в 22.0 MiB/24 MiB. Предел - это UPPER BOUND, сколько памяти вы можете выделить. Это то, что меня бросило на некоторое время. Крушение происходит значительно до того, как вы достигнете предела. Таким образом, необходимость в значении "memoryPad" в решении, как попытка выделить 23.999 MiB/24 MiB, приведет к сбою почти в 100% случаев. Итак, если предел равен 24 миллионам, сколько можно безопасно использовать? Неизвестный. Кажется, что работает 20 MiB. 22 MiB, похоже, работает. Я нервничаю, приближаюсь к ней ближе. Амплитуда варьируется в зависимости от того, насколько фрагментировано пространство памяти malloc в собственном процессе. И, конечно, нет никакого способа измерить это, так что ошибайтесь на безопасной стороне.

07-31 18:37:19.031: WARN/Epic(3118): MEMORY-USED: 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: INFO/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.00M
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-byte external allocation too large for this process.
07-31 18:37:19.081: ERROR/dalvikvm(3118): Out of memory: Heap Size=6535KB, Allocated=4247KB, Bitmap Size=17767KB
07-31 18:37:19.081: ERROR/GraphicsJNI(3118): VM won't let us allocate 1111200 bytes

Код для распечатки всего:


    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);
    }