Límite de mapa de bits de Android: evitando java.lang.OutOfMemory

Actualmente estoy luchando con un comportamiento extraño de la plataforma Android – el límite de memoria de montón de mapas de bits / Java. Según el dispositivo, Android limita el desarrollador de la aplicación a 16, 24 o 32 MiB de espacio de montón de Java (o puede encontrar cualquier valor aleatorio en un teléfono con raíz). Esto es posiblemente bastante pequeño, pero relativamente sencillo, ya que puedo medir el uso con las siguientes API:

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

Suficientemente fácil; Ahora para el giro. En Android, los mapas de bits, con pocas excepciones, se almacenan en el montón nativo y no cuentan para el montón de Java. Un desarrollador de ojos claros y purista de Google decidió que esto era "malo" y le permitió al desarrollador obtener "más que su parte justa". Así que hay este pequeño pedazo de código que calcula el uso de la memoria nativa incurridos por los mapas de bits, y posiblemente otros recursos, y las sumas que con el montón de Java y si vas más ….. java.lang.OutOfMemory. Ay

Pero no es gran cosa. Tengo muchos mapas de bits, y no necesito todos ellos todo el tiempo. Puedo "descargar" algunos de los que no están siendo utilizados en este momento:

Por lo tanto, para el intento # 1, refactoreé el código para poder envolver cada carga de mapa de bits con 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 } } 

Vea, he aquí un pequeño fragmento de registro que muestra mi código captando la excepción y reciclando algunos mapas de bits:

 E / Epic (23221): OUT_OF_MEMORY (capturado 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 (esto es 4 * #pixels en todos los bitmaps cargados)
 I / Epic (23221): NativeHeapSize = 29.4M
 I / Epic (23221): NativeHeapAllocSize = 25.2M
 I / Epic (23221): ThreadAllocSize = 0.00M
 I / Epic (23221): totalMemoria () = 9,1M
 I / Epic (23221): maxMemory () = 32.0M
 I / Epic (23221): freeMemory () = 4,4M
 W / Epic (23221): Reciclaje de mapa de bits 'game_word_puzzle_11_aniframe_005'
 I / Epic (23221): BITMAP_RECYCLING: 1 bitmaps reciclado de 1.1M).  Edad = 294

Tenga en cuenta cómo totalMemory – freeMemory es sólo 4,7 MiB, pero con ~ 26? MiB de la memoria nativa ocupada por los mapas de bits, estamos en la gama 31/32 MiB donde alcanzamos el límite. Todavía estoy un poco confundido aquí como mi cuenta corriente de todos los bitmaps cargados es 26.6 MiB, con todo el tamaño nativo del alloc es solamente 25.2 MiB. Así que estoy contando algo mal. Pero está todo en el estadio de béisbol y demuestra claramente la "sumación" de la piscina cruzada que sucede con el límite del mem.

Pensé que lo había arreglado. Pero no, Android no se rendiría tan fácilmente …

Esto es lo que obtengo de dos de mis cuatro dispositivos de prueba:

 I / dalvikvm-heap (17641): Sujete el montón de GC objetivo de 32.687MB a 32.000MB
 D / dalvikvm (17641): GC_FOR_MALLOC liberado <1K, 41% libre 4684K / 7815K, externo 24443K / 24443K, pausado 24ms
 D / dalvikvm (17641): GC_EXTERNAL_ALLOC liberado <1K, 41% libre 4684K / 7815K, externo 24443K / 24443K, pausado 29ms
 E / dalvikvm-heap (17641): 1111200-byte asignación externa demasiado grande para este proceso.
 E / dalvikvm (17641): Fuera de memoria: Tamaño de montón = 7815 KB, asignado = 4684 KB, tamaño de mapa de bits = 24443 KB, límite = 32768 KB
 E / dalvikvm (17641): Información del recorte: Huella = 7815KB, huella permitida = 7815KB, recortada = 880KB
 E / GraphicsJNI (17641): VM no nos permite asignar 1111200 bytes
 I / dalvikvm-heap (17641): Sujete el montón de GC objetivo de 32.686MB a 32.000MB
 D / dalvikvm (17641): GC_FOR_MALLOC liberado <1K, 41% libre 4684K / 7815K, externo 24443K / 24443K, pausado 17ms
 I / DEBUG (1505): *** *** *** *** *** *** *** *** *** *** *** *** *** *** * 2
 I / DEBUG (1505): Construir huella dactilar: 'verizon_wwe / htc_mecha / mecha: 2.3.4 / GRJ22 / 98797: user / release-keys'
 I / DEBUG (1505): pid: 17641, tid: 17641
 I / DEBUG (1505): señal 11 (SIGSEGV), código 1 (SEGV_MAPERR), error 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

Eso es un accidente nativo. A segfault no menos (sig11). Por definición, un segfault es SIEMPRE un error. Esto es absolutamente un error de Android en el código nativo que controla la GC y / o la verificación del límite de memoria. Pero sigue siendo mi aplicación que se bloquea dando lugar a malas revisiones, devoluciones y ventas más bajas.

Así que tengo que calcular el límite yo mismo. Excepto que he luchado aquí. Traté de añadir los píxeles yo mismo (EpicPixels), pero todavía golpeó el memcrash periódicamente, por lo que estoy undercounting algo. Intenté agregar los javaBytes (total – free) a NativeHeapAllocSize, pero esto ocasionaría que mi aplicación se volviera "anoréxica", liberando y liberando bitmaps hasta que no quedara nada para purgar.

  1. ¿Alguien sabe el cálculo exacto utilizado para calcular el límite de memoria y desencadenar java.lang.OutOfMemory?

  2. ¿Alguien más golpeó este problema y trabajó a través de él? ¿Tienes alguna perla de sabiduría?

  3. ¿Alguien sabe qué empleado de Google soñó este esquema para que yo pueda darle un puñetazo por arruinar ~ 40 horas de mi vida? Jk

RESPUESTA: El límite es para NativeHeapAllocSize <maxMemory (); Sin embargo, debido a la fragmentación de memoria, Android se bloquea mucho antes del límite real. Por lo tanto, usted tiene que limitarse a un valor algo menos que el límite real. Este "factor de seguridad" depende de la aplicación, pero algunos MiB parecen funcionar para la mayoría de las personas. (Puedo apenas decir que estoy asombrado por cómo está roto este comportamiento es)

Utilice este snipplet, trabajado para mí

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

Aquí hay un ejemplo de uso

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

El límite varía en cada dispositivo (utilice el tercer enlace si desea cargar el mapa de bits como está) o aquí tiene algunos trucos para evitar que el problema como:

  • Utilice onLowMemory () de la clase Application para liberar algo de memoria evitando el bloqueo.
  • Indique el tamaño deseado para el mapa de bits antes de decodificarlo. Compruebe que los enlaces para obtener más información:

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

Problema extraño de memoria al cargar una imagen en un objeto Bitmap

Este enlace muestra para comprobar el montón

BitmapFactory OOM me conduce nueces

  • Y por supuesto libre de la memoria de mapas de bits antiguos

Aceptar, así que estoy empezando a sospechar que el límite en modo nativo se aplica en el tamaño total de java montón + memoria utilizada nativa.

El límite se basa en NativeHeapAllocSize vs. maxMemory (). Usted verá a continuación que estoy aplastando asignando ~ 1 MiB mientras estoy en 22.0 MiB / 24 MiB. El límite es UPPER BOUND en la cantidad de memoria que puede asignar. Esto es lo que me echó por un tiempo. El accidente ocurre de manera significativa antes de alcanzar el límite. Por lo tanto, la necesidad de un "memoryPad" valor en la solución, como tratando de asignar 23.999 MiB / 24 MiB dará lugar a un accidente casi el 100% del tiempo. Así que si el límite es de 24 MiB, ¿cuánto puede utilizar con seguridad? Desconocido. 20 MiB parece funcionar. 22 MiB parece funcionar. Estoy nervioso empujando más cerca que eso. La cantidad varía dependiendo de cuán fragmentado esté el espacio de memoria malloc en el proceso nativo. Y, por supuesto, no hay manera de medir nada de esto, así que errar en el lado seguro.

 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 asignación externa demasiado grande para este proceso.
 07-31 18: 37: 19.081: ERROR / dalvikvm (3118): Fuera de memoria: Heap Size = 6535KB, asignado = 4247KB, tamaño de mapa de bits = 17767KB
 07-31 18: 37: 19.081: ERROR / GraphicsJNI (3118): VM no nos permite asignar 1111200 bytes

El código para imprimir todo eso:

 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); } 
  • Gran cantidad de memoria no recogida de basura
  • Cargar un recurso a un mapa de bits mutable
  • Señal fatal 11 (SIGSEGV) en 0x00000000 (código = 1), hilo 27830 (ple.myfragexample) - sólo en Android 4.1.2
  • Outofmemoryerror al hacer bitmap
  • Android procesamiento de imágenes de alta resolución
  • OutOfMemoryError al cargar imágenes en RecyclerView
  • Error de memoria de imagen de mapa de bits de Android
  • API básica de Android v2 MapActivity outOfMemory con 10 marcadores
  • Aceleración de hardware OOM androide nativo heap
  • Android java.lang.OutOfMemoryError
  • OutOfMemoryException al cargar imágenes de gran tamaño usando glide
  • FlipAndroid es un fan de Google para Android, Todo sobre Android Phones, Android Wear, Android Dev y Aplicaciones para Android Aplicaciones.