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