2011-07-31 9 views
24

Actualmente estoy teniendo problemas con un comportamiento extraño de la plataforma Android: el límite de memoria de montón de mapa de bits/Java. Dependiendo del dispositivo, Android limita el desarrollador de la aplicación a 16, 24 o 32 MiB de espacio de almacenamiento dinámico de Java (o puede encontrar cualquier valor aleatorio en un teléfono rooteado). Este es sin duda bastante pequeño, pero relativamente sencillo como puedo medir el uso de la API de los siguientes:Límite de mapa de bits de Android - Prevención de java.lang.OutOfMemory

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

bastante fácil; ahora para el giro. En Android, los mapas de bits, con algunas excepciones, se almacenan en el montón nativo y no cuentan para el montón de Java. Un desarrollador purista y de ojos brillantes en Google decidió que esto era "malo" y le permitió al desarrollador obtener "más de lo que le corresponde". Así que hay una pequeña porción de código que calcula el uso de la memoria nativa en mapas de bits, y posiblemente otros recursos, y suma eso con el montón de Java y si se pasa a ... java.lang.OutOfMemory. ouch

Pero no es gran cosa. Tengo muchos mapas de bits, y no los necesito todos en todo momento. Puedo "localizar" algunos de los que no se están utilizando en este momento:

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

Sede, aquí hay un buen fragmento pequeño de registro que muestre mi código agarrar la excepción, y el reciclaje de algunos mapas de bits:

 
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 

Nota cómo totalMemory - FreeMemory es sólo el 4,7 MiB, pero con ~ 26? MiB de memoria nativa tomada por bitmaps, estamos en el rango de 31/32 MiB donde alcanzamos el límite. Todavía estoy un poco confundido aquí ya que mi cuenta corriente de todos los bitmaps cargados es de 26.6 MiB, sin embargo, el tamaño del alloc nativo es de solo 25.2 MiB. Así que estoy contando algo mal. Pero todo está en el estadio de béisbol y definitivamente demuestra la "suma" cruzada que ocurre con el límite de la memoria.

PENSÉ que lo tenía arreglado. Pero no, Android no se rendiría tan fácilmente ...

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

 
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 

Eso es un accidente nativa. Un segfault no menos (sig11). Por definición, un segfault es SIEMPRE un error. Esto es absolutamente un error de Android en el manejo del código nativo de GC y/o la comprobación de límites de mem. Pero sigue siendo mi aplicación la que falla, lo que resulta en malas críticas, devoluciones y ventas más bajas.

Así que tengo que calcular el límite yo mismo. Excepto que he luchado aquí. Traté de sumar los píxeles yo mismo (EpicPixels), pero todavía golpeo el memcrash periódicamente, así que no estoy contando nada. Traté de agregar javaBytes (total - gratis) a NativeHeapAllocSize, pero ocasionalmente causó que mi aplicación se volviera "anoréxica", liberando y liberando bitmaps hasta que no quedaba nada por purgar.

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

  2. ¿Alguien más ha solucionado este problema y ha solucionado el problema? ¿Tienes alguna perla de sabiduría?

  3. ¿Alguien sabe qué empleado de Google soñó con este plan para que yo pueda golpearlo por arruinar ~ 40 horas de mi vida? j/k

RESPUESTA: El límite es para NativeHeapAllocSize < MaxMemory(); sin embargo, debido a la fragmentación de la memoria, Android se bloquea mucho antes del límite real. Por lo tanto, debe limitarse a un valor algo menor que el límite real. Este "factor de seguridad" depende de la aplicación, pero unos pocos MiB parecen funcionar para la mayoría de las personas. (¿Puedo decir que estoy impresionado por lo roto que está este comportamiento?)

Respuesta

18

Utilice esta 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í es 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;   
     } 
+0

Esto es muy similar al código que terminé usando. En lugar de devolver verdadero/falso, uso bitmap.recycle() para liberar espacio ... –

+1

Lo intenté ... pero si uso checkBitmapFitsInMemory() devuelve falso y si no uso checkBitmapFitsInMemory() la aplicación carga mapa de bits sin ningún bloqueo. Mis valores registrados: bmpwidth = 93, bmpheight = 131, bmpdensity = 2, reqsize = 24366, heapPad = 4194304, allocNativeHeap = 33150304, maxMemory = 33554432. ¿Alguna idea sobre cómo escribir un método similar, pero que funciona mejor? – Geltrude

+2

Este código no funciona en Honeycomb y más, ya que los mapas de bits ya no se almacenan en el montón nativo. – vomitcuddle

3

El límite varía según cada dispositivo (utilice el tercer enlace si desea cargar el mapa de bits como está) o aquí tiene algunos trucos para evite ese problema como:

  • use onLowMemory() de la clase Application para liberar memoria evitando el bloqueo.
  • indicar el tamaño deseado para el mapa de bits antes de la decodificación de la misma. Compruebe que los enlaces para más información:

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

Strange out of memory issue while loading an image to a Bitmap object

Este espectáculo enlace para comprobar el montón

BitmapFactory OOM driving me nuts

  • Y, por supuesto, liberar la memoria de los viejos mapas de bits
+2

Probé Application.onLowMemory(). Desafortunadamente, nunca se llama en mi repro del crash segfault. –

2

Bien, entonces estoy empezando a sospechar que el límite en el modo nativo se aplica en total tamaño de almacenamiento Java + memoria utilizada nativa.

El límite se basa en NativeHeapAllocSize frente a maxMemory(). Verás a continuación que estoy bloqueando la asignación de ~ 1 MiB mientras estoy en 22.0 MiB/24 MiB. El límite es un límite superior de la cantidad de memoria que puede asignar. Esto es lo que me tiró por un tiempo. El accidente ocurre significativamente antes de llegar al límite. Por lo tanto, la necesidad de un valor de "memoryPad" en la solución, ya que tratar de asignar 23.999 MiB/24 MiB provocará un bloqueo casi el 100% del tiempo. Entonces, si el límite es de 24 MiB, ¿cuánto puede usar con seguridad? Desconocido. 20 MiB parece funcionar. 22 MiB parece funcionar. Estoy nervioso empujando más cerca que eso. La cantidad varía según cuán fragmentado esté el espacio de memoria malloc en el proceso nativo. Y, por supuesto, no hay forma de medir nada de esto, así que erré por 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 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 

El código para imprimir todo lo que fuera:


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

¿Qué es "toMib"? –

+0

@dpk - toMib es un ayudante que formatea los bytes grandes en más amigables "12.3 MiB" ...MiB es la forma más pedante de MB que implica base-1024 unidades SI en lugar de base-1000. Probablemente no me di cuenta de que lo usé y no lo pegué. Puede eliminarlo o convertirlo en su propio formato favorito sin perder la intención del código. –

Cuestiones relacionadas