¿Cómo funciona la asignación de mapas de bits en Oreo y cómo investigar su memoria?

Antecedentes

En los últimos años, para verificar cuánta memoria de almacenamiento dinámico tiene en Android y cuánto usa, puede usar algo como:

@JvmStatic
fun getHeapMemStats(context: Context): String {
    val runtime = Runtime.getRuntime()
    val maxMemInBytes = runtime.maxMemory()
    val availableMemInBytes = runtime.maxMemory() - (runtime.totalMemory() - runtime.freeMemory())
    val usedMemInBytes = maxMemInBytes - availableMemInBytes
    val usedMemInPercentage = usedMemInBytes * 100 / maxMemInBytes
    return "used: " + Formatter.formatShortFileSize(context, usedMemInBytes) + " / " +
            Formatter.formatShortFileSize(context, maxMemInBytes) + " (" + usedMemInPercentage + "%)"
}

Esto significa que mientras más memoria use, especialmente al almacenar mapas de bits en la memoria, más cerca estará de la memoria de almacenamiento dinámico máxima que su aplicación puede usar. Cuando alcance el máximo, su aplicación se bloqueará con la excepción OutOfMemory (OOM).

El problema

Me di cuenta de que en Android O (8.1 en mi caso, pero probablemente también en 8.0), el código anterior no se ve afectado por las asignaciones de mapas de bits.

Excavando aún más, noté en el perfilador de Android que cuanta más memoria use (guardando mapas de bits grandes en mi POC), más memoria nativa se usa.

Para probar cómo funciona, he creado un bucle simple como tal:

    val list = ArrayList<Bitmap>()
    Log.d("AppLog", "memStats:" + MemHelper.getHeapMemStats(this))
    useMoreMemoryButton.setOnClickListener {
        AsyncTask.execute {
            for (i in 0..1000) {
                // list.add(Bitmap.createBitmap(20000, 20000, Bitmap.Config.ARGB_8888))
                list.add(BitmapFactory.decodeResource(resources, R.drawable.huge_image))
                Log.d("AppLog", "heapMemStats:" + MemHelper.getHeapMemStats(this) + " nativeMemStats:" + MemHelper.getNativeMemStats(this))
            }
        }
    }

En algunos casos, lo hice en una sola iteración, y en algunos, solo creé un mapa de bits en la lista, en lugar de decodificarlo (código en el comentario). Más sobre esto más tarde ...

Este es el resultado de ejecutar lo anterior:

Como puede ver en el gráfico, la aplicación alcanzó un gran uso de memoria, muy por encima de la memoria de almacenamiento dinámico máxima permitida que me informaron (que es 201 MB).

Lo que he encontrado

He encontrado muchos comportamientos extraños. Debido a esto, he decidido informar sobre ellos,aquí.

Primero, probé una alternativa al código anterior, para obtener las estadísticas de memoria en tiempo de ejecución:

 @JvmStatic
 fun getNativeMemStats(context: Context): String {
     val nativeHeapSize = Debug.getNativeHeapSize()
     val nativeHeapFreeSize = Debug.getNativeHeapFreeSize()
     val usedMemInBytes = nativeHeapSize - nativeHeapFreeSize
     val usedMemInPercentage = usedMemInBytes * 100 / nativeHeapSize
     return "used: " + Formatter.formatShortFileSize(context, usedMemInBytes) + " / " +
             Formatter.formatShortFileSize(context, nativeHeapSize) + " (" + usedMemInPercentage + "%)"
 }

Pero, a diferencia de la comprobación de la memoria de almacenamiento dinámico, parece que la memoria nativa máxima cambia su valor con el tiempo, lo que significa que no puedo saber cuál es su verdadero valor máximo y, por lo tanto, en aplicaciones reales, no puedo decidir qué El tamaño de la memoria caché debe ser. Aquí está el resultado del código anterior:

heapMemStats:used: 2.0 MB / 201 MB (0%) nativeMemStats:used: 3.6 MB / 6.3 MB (57%)
heapMemStats:used: 1.8 MB / 201 MB (0%) nativeMemStats:used: 290 MB / 310 MB (93%)
heapMemStats:used: 1.8 MB / 201 MB (0%) nativeMemStats:used: 553 MB / 579 MB (95%)
heapMemStats:used: 1.8 MB / 201 MB (0%) nativeMemStats:used: 821 MB / 847 MB (96%)

Cuando llego al punto de que el dispositivo no puede almacenar más mapas de bits (detenido en 1.1GB o ~ 850MB en mi Nexus 5x), en lugar de la excepción OutOfMemory, obtengo ... ¡nada! Simplemente cierra la aplicación. Sin siquiera un diálogo que diga que se ha estrellado.

En caso de que solo cree un nuevo mapa de bits, en lugar de decodificarlo (código disponible arriba, solo en un comentario), recibo un registro extraño que dice que uso toneladas de GB y tengo toneladas de GB de memoria nativa disponibles:

Además, a diferencia de cuando decodifico mapas de bits, aquí me cuelgo (incluido un diálogo), pero no es OOM. En cambio, es ... NPE!

01-04 10: 12: 36.936 30598-31301 / com.example.user.myapplication E / AndroidRuntime: EXCEPCIÓN FATAL: AsyncTask Proceso 1: com.example.user.myapplication, PID: 30598 java.lang.NullPointerException: Intento de invoque el método virtual 'void android.graphics.Bitmap.setHasAlpha (boolean)' en una referencia de objeto nulo en android.graphics.Bitmap.createBitmap (Bitmap.java:1046) en android.graphics.Bitmap.createBitmap (Bitmap.java:980 ) en android.graphics.Bitmap.createBitmap (Bitmap.java:930) en android.graphics.Bitmap.createBitmap (Bitmap.java:891) en com.example.user.myapplication.MainActivity $ onCreate $ 1 $ 1.run (MainActivity. kt: 21) en android.os.AsyncTask $ SerialExecutor $ 1.run (AsyncTask.java:245) en java.util.concurrent.ThreadPoolExecutor.runWorker (ThreadPoolExecutor.java:1162) en java.util.concurrent.ThreadPoolExecutor ejecutar (ThreadPoolExecutor.java:636) en java.lang.Thread.run (Thread.java:764)

Mirando el gráfico del generador de perfiles, se vuelve aún más extraño. El uso de memoria no parece aumentar mucho, y en el punto de bloqueo, simplemente cae:

Si observa el gráfico, verá muchos iconos de GC (el bote de basura). Creo que podría estar haciendo algo de compresión de memoria.

Si hago un volcado de memoria (usando el generador de perfiles), a diferencia de las versiones anteriores de Android, ya no puedo ver una vista previa de Bitmaps.

Las preguntas

Este nuevo comportamiento plantea muchas preguntas. Podría reducir el número de bloqueos de OOM, pero también puede hacer que sea muy difícil detectarlos, encontrar fugas de memoria y repararlos. Tal vez algo de lo que he visto son solo errores, pero aún así ...

¿Qué ha cambiado exactamente en el uso de memoria en Android O? ¿Y por qué?

¿Cómo se manejan los mapas de bits?

¿Es posible obtener una vista previa de mapas de bits dentro de los informes de volcado de memoria?

¿Cuál es la forma correcta de obtener la memoria nativa máxima que la aplicación puede usar, e imprimirla en los registros, y usarla como algo para decidir el máximo?

¿Hay algún video / artículo sobre este tema? No estoy hablando de optimizaciones de memoria que se agregaron, sino más sobre cómo se asignan los mapas de bits ahora, cómo manejar OOM ahora, etc.

Supongo que este nuevo comportamiento podría afectar algunas bibliotecas de almacenamiento en caché, ¿verdad? Eso es porque podrían depender del tamaño de la memoria de almacenamiento dinámico en su lugar.

¿Cómo podría ser que pudiera crear tantos mapas de bits, cada uno de tamaño 20,000x20,000 (que significa ~ 1.6 GB), pero cuando solo podía crear algunos de ellos a partir de una imagen real de tamaño 7,680x7,680 (que significa ~ 236MB) ? ¿Realmente hace compresión de memoria, como he adivinado?

¿Cómo podrían las funciones de memoria nativas devolverme valores tan enormes en el caso de la creación de mapas de bits, pero aún más razonables para cuando decodifiqué mapas de bits? ¿Qué quieren decir?

¿Qué pasa con el extraño gráfico de perfil en el caso de creación de mapa de bits? Apenas aumenta en el uso de la memoria y, sin embargo, llegó a un punto en el que no puede crear más, eventualmente (después de que se insertaron muchos elementos).

¿Qué pasa con el extraño comportamiento de las excepciones? ¿Por qué en la decodificación de mapas de bits no obtuve ninguna excepción o incluso un registro de errores como parte de la aplicación, y cuando los creé, obtuve NPE?

¿Play Store detectará OOM y aún informará sobre ellos, en caso de que la aplicación se bloquee debido a eso? ¿Lo detectará en todos los casos? ¿Crashlytics puede detectarlo? ¿Hay alguna manera de estar informado de tal cosa, ya sea por los usuarios o durante el desarrollo en la oficina?