Los objetos de mapa de bits suelen ser los que más contribuyen al espacio en memoria de una aplicación. Ya sean íconos de apps, imágenes de notificaciones o contenido multimedia, el manejo ineficiente de mapas de bits puede provocar rápidamente errores de falta de memoria (OOM) y presión de memoria en todo el sistema.
Configuraciones de mapa de bits y datos de píxeles
La cantidad de memoria que consume un mapa de bits está determinada principalmente por sus dimensiones (ancho × alto) y su configuración (Bitmap.Config).
La configuración define cuántos bytes se usan para representar cada píxel:
| Configuración | Bytes por píxel | Descripción |
|---|---|---|
ALPHA_8 |
1 | Solo canal alfa (transparencia). Es útil para máscaras. |
RGB_565 |
2 | Rojo (5 bits), verde (6 bits) y azul (5 bits). Sin alfa. Es adecuado para imágenes opacas en las que la alta fidelidad de color no es fundamental. |
ARGB_8888 |
4 | Alfa, rojo, verde y azul (8 bits cada uno). Es el valor predeterminado y el más común. |
RGBA_F16 |
8 | Punto flotante de media precisión. Se usa para contenido HDR y de gama amplia. |
HARDWARE |
N/A | Se almacena en la memoria de gráficos (gralloc/DMABuf). Consulta Mapas de bits de hardware. |
Fórmula de memoria: Memory (Bytes) = Width × Height × Bytes Per Pixel
Por ejemplo, una imagen de pantalla completa en un dispositivo de 1080p (1920 x 1080) en ARGB_8888 ocupa: 1920 × 1080 × 4 bytes ≈ 8.3 MB.
Mapas de bits de montón vs. mapas de bits compartidos
Mapas de bits de montón (montón nativo)
En Android moderno (8.0 y versiones posteriores), los datos de píxeles del mapa de bits se almacenan en el montón nativo, mientras que solo un pequeño objeto contenedor reside en el montón de Java.
Cuando una app necesita presentar una imagen, por lo general, se decodifica de un archivo de imagen comprimido a un mapa de bits y se almacena en el montón.
Mapas de bits compartidos (ashmem/memfd)
Cuando se transfiere un mapa de bits entre procesos (p.ej., a través de Binder a SystemUI para una notificación), Android evita copiar los datos de píxeles mediante el uso de memoria compartida (ashmem o memfd).
Se puede copiar una instancia de Bitmap a la memoria compartida de forma explícita llamando a
Bitmap.asShared(),
o de forma implícita si se coloca un Bitmap dentro de un Parcel (por lo general, agregando el
mapa de bits a un Parcelable, como un Bundle) y se envía a través de Binder IPC.
Cuando se envía un mapa de bits compartido a través de Binder IPC, los datos de píxeles en sí no se copian, sino que se duplica un descriptor de archivo que hace referencia a una región de memoria compartida en el proceso del destinatario. La región de memoria subyacente se puede compartir entre varios procesos y no se libera hasta que se cierran todos los descriptores de archivo que hacen referencia a ella.
Mapas de bits mutables vs. inmutables
- Mapas de bits mutables: Se pueden modificar después de la creación (p.ej., a través de un
Canvas). Siempre requieren su propia asignación de memoria privada. Si se copia un mapa de bits mutable, se debe realizar una copia profunda (segunda copia de todos los datos de píxeles). - Mapas de bits inmutables: No se pueden cambiar. Esto permite optimizaciones, como compartir el mismo búfer de memoria subyacente entre diferentes instancias de
Bitmap. Los mapas de bits cargados desde recursos de APK (BitmapFactory) suelen ser inmutables.
Manejo eficiente de mapas de bits
Agrupación y reutilización de mapas de bits
La asignación y la desasignación de mapas de bits con frecuencia provocan una rotación de asignación, lo que obliga a la GC a ejecutarse constantemente. Las bibliotecas comunes de carga de imágenes usan un grupo de mapas de bits.
Google recomienda Glide como solución para aplicaciones basadas en Java y Coil para aplicaciones basadas en Kotlin (en especial, cuando se usa Jetpack Compose).
Cuando ya no se necesita un mapa de bits, en lugar de dejar que se realice la GC, la app llama a bitmap.recycle() o lo devuelve a un grupo. La próxima vez que se necesite un mapa de bits de las mismas dimensiones y configuración, el grupo proporcionará el búfer existente, lo que evitará una nueva asignación.
Mapas de bits de hardware
Bitmap.Config.HARDWARE te permite almacenar datos de píxeles directamente en la memoria de gráficos (DMABuf).
- Ventajas:
- Ahorro de memoria: No usa la aplicación ni el montón nativo; usa la memoria de la GPU. A menudo, los mapas de bits que se muestran en la IU de una app deben copiarse en la memoria de la GPU de todos modos, por lo que esto guarda esa operación de copia y el costo de memoria adicional.
- Rendimiento: Es extremadamente rápido de dibujar porque los datos ya están en la GPU.
- Desventajas:
- Inmutable: No se pueden modificar los mapas de bits de hardware.
- La lectura es lenta: El acceso a los píxeles desde la CPU (p.ej.,
getPixel()) es muy costoso. - Atribución: Es más difícil realizar un seguimiento en herramientas estándar como AHAT (consulta más abajo).
Ejercicio práctico: exploración de mapas de bits
Usaremos la app de ejemplo BitmapLab para explorar estos conceptos.
1. Medición con dumpsys meminfo
Inicia BitmapLab y presiona ALLOCATE 10MB ARGB_8888. Luego, ejecuta lo siguiente:
adb shell dumpsys meminfo -s com.android.bitmaplab
En las versiones modernas de Android, busca la sección Native Allocations. Estos proporcionan una atribución mucho mejor para los mapas de bits que el Resumen de la app:
Native Allocations
Count Total(kB)
------ ------
Bitmap (malloced): 1 10240 # <--- 10MB Bitmap data!
Bitmap (nonmalloced): 0 0
- Bitmap (malloced): Mapas de bits asignados en el montón nativo del proceso. Aquí es donde residen la mayoría de los mapas de bits estándar en Android 8.0 y versiones posteriores.
- Bitmap (nonmalloced): Mapas de bits que usan memoria especializada, como
mapas de bits de hardware o mapas de bits compartidos (a través de
ashmemomemfd).
Si asignas un mapa de bits compartido en BitmapLab, lo verás reflejado
en Bitmap (nonmalloced):
Native Allocations
Count Total(kB)
------ ------
Bitmap (malloced): 1 10240
Bitmap (nonmalloced): 1 10240 # <--- Shared Bitmap!
Seguimiento de mapas de bits compartidos
En algunas versiones de Android y configuraciones de kernel, dumpsys meminfo también proporciona un seguimiento de alta resolución para los mapas de bits que se asignan al espacio de direcciones del proceso a través de descriptores de archivo.
De forma predeterminada, los mapas de bits compartidos usan un nombre genérico ("bitmap"). Para habilitar la atribución detallada y el seguimiento único de mapas de bits (identificar mapas de bits compartidos en diferentes procesos), debes habilitar la siguiente propiedad del sistema:
adb shell setprop debug.hwui.bitmap_ashmem_long_name true
Cuando esté habilitado, las regiones de ashmem en /proc/<pid>/smaps tendrán nombres más
descriptivos. meminfo aprovechará eso, y los resultados se verán de la siguiente manera:
Shared Bitmaps
Count Size(KB)
------ ------
Mapped: 1 10240
Unique: 1 10240
- Mapped: Es el tamaño total de todas las asignaciones de memoria relacionadas con el mapa de bits.
- Único: Es el tamaño de los mapas de bits que solo consideran los únicos (es decir, dos o más asignaciones de los mismos datos de píxeles de mapa de bits compartidos subyacentes solo se cuentan una vez).
2. Mapas de bits en AHAT
AHAT proporciona una excelente visualización para los mapas de bits.
- En BitmapLab, asigna algunos mapas de bits.
Captura un volcado de montón con la marca
-b(para incluir datos de mapa de bits nativos):adb shell am dumpheap -b png com.android.bitmaplab /data/local/tmp/bitmaps.hprof adb pull /data/local/tmp/bitmaps.hprof . ahat bitmaps.hprofAbre
localhost:7100y busca el vínculo Bitmaps en la barra lateral o busca la claseBitmap.AHAT renderizará los mapas de bits en el navegador, lo que facilitará la identificación de las imágenes que acaparan la memoria.

3. Registros de mapas de bits en Perfetto
Perfetto puede hacer un seguimiento de las asignaciones y los recuentos de mapas de bits a lo largo del tiempo. El framework de Android emite estos contadores cuando se habilita la categoría gfx de atrace para una aplicación específica.
Inicia un registro. Debes incluir la categoría
gfxy segmentar el paquete de app específico con la marca-a:external/perfetto/tools/record_android_trace -o bitmaps.perfetto-trace \ -t 15s -b 64mb view gfx dalvik am res memory -a com.android.bitmaplabEn BitmapLab, presiona los botones Allocate y Clear varias veces.
También presiona Parcel/Unparcel Bitmap.
Analiza el registro en ui.perfetto.dev.
En la sección de procesos para com.android.bitmaplab, verás lo siguiente:
* Bitmap Count: Es un contador que muestra la cantidad de mapas de bits activos.
* Bitmap Memory: Es un contador que muestra los bytes totales que usan los mapas de bits.
Segmentos de alto nivel (SDK de Perfetto)
BitmapLab también usa el SDK de Perfetto para emitir segmentos de alto nivel para las operaciones de mapa de bits. Busca BitmapLab_ en el registro para encontrar lo siguiente:
* BitmapLab_parcelUnparcel: Segmentos que abarcan la lógica de empaquetado y desempaquetado
logic.
* BitmapLab_postNotification: Segmentos que abarcan el flujo de publicación de notificaciones.
Seguimiento de flujos de notificación
Cuando presionas Post Notification, la app crea una notificación que contiene el mapa de bits actual y la envía al sistema. El código del framework responsable de esto emite segmentos de Perfetto con eventos de flujo que conectan el empaquetado (escribir el mapa de bits en un paquete para enviarlo a través de Binder IPC) y el desempaquetado (leer el mapa de bits de un paquete en el extremo receptor).
En la siguiente captura de pantalla, puedes ver la app que empaqueta el mapa de bits grande para usarlo en una transacción de Binder para publicar la notificación y el desempaquetado correspondiente en el proceso system_server.

Con Perfetto, incluso puedes seguir el mismo mapa de bits de notificación a medida que se propaga aún más a través de subprocesos y procesos, por ejemplo, desde un subproceso de Binder en system_server (que implementa el servidor de Binder INotificationManager) hasta subprocesos de trabajo system_server que podrían reenviar el mismo mapa de bits a com.android.systemui para que se muestre en el panel de notificaciones.
Desafíos de las apps del sistema
Las apps del sistema, como SystemUI (Notificaciones) y Launcher , enfrentan desafíos únicos:
- Contenido sin límites: Las notificaciones y los widgets pueden ser numerosos. Si cada uno contiene un mapa de bits grande, el sistema puede quedarse sin memoria rápidamente.
- Duplicación: El mismo ícono de la app se puede guardar en la caché de Launcher, el área de notificaciones de SystemUI y la app de Configuración.
- Uso compartido a través de búferes de hardware: Para mitigar esto, los componentes del sistema se están
moviendo hacia un servicio centralizado de "descarga de imágenes" que comparte
HardwareBufferinstancias en todos los procesos. Atribución de DMABuf: Los mapas de bits de hardware guardan espacio de montón, pero usan memoria DMABuf, que es más difícil de atribuir a un proceso específico en herramientas de memoria estándar.
Usa
adb shell dmabuf_dumppara ver las asignaciones de DMABuf en todo el sistema. Esta herramienta proporciona un desglose de los búferes por proceso:droid.bitmaplab:19562 Name Rss Pss nr_procs Inode Exporter <unknown> 3840 kB 1280 kB 3 3397 virtio_gpu system 12 kB 4 kB 3 3398 system <unknown> 3840 kB 1920 kB 2 3399 virtio_gpu system 12 kB 6 kB 2 3400 system PROCESS TOTAL 11556 kB 5136 kB- RSS: Es el tamaño total del búfer si se asigna en el proceso.
- Pss: Es el tamaño proporcional (RSS dividido por la cantidad de procesos que comparten el búfer). Esta es la mejor métrica para la contabilidad.
- nr_procs: Es la cantidad de procesos que actualmente tienen una referencia a este búfer.
- Exporter: Es el controlador que asignó el búfer (p.ej.,
virtio_gpuen Cuttlefish o un montón de Ion/DMA-BUF específico del proveedor en el hardware).
También puedes usar
adb shell dmabuf_dump -bpara obtener un resumen de todos los búferes y el uso total de DMA-BUF en todo el sistema.