Bitmaps et mémoire

Les objets bitmap sont souvent les principaux contributeurs à l'espace mémoire utilisé d'une application. Qu'il s'agisse d'icônes d'application, d'images de notification ou de contenu multimédia, une gestion inefficace des bitmaps peut rapidement entraîner des erreurs de mémoire insuffisante et une pression sur la mémoire à l'échelle du système.

Configurations bitmap et données de pixels

La quantité de mémoire consommée par un bitmap est principalement déterminée par ses dimensions (largeur × hauteur) et sa configuration (Bitmap.Config).

La configuration définit le nombre d'octets utilisés pour représenter chaque pixel :

Configuration Octets par pixel Description
ALPHA_8 1 Canal alpha (transparence) uniquement. Utile pour les masques.
RGB_565 2 Rouge (5 bits), vert (6 bits), bleu (5 bits). Pas d'alpha. Convient aux images opaques où la fidélité des couleurs n'est pas essentielle.
ARGB_8888 4 Alpha, rouge, vert, bleu (8 bits chacun). Par défaut et le plus courant.
RGBA_F16 8 Virgule flottante semi-précision. Utilisé pour le contenu à large gamme de couleurs et HDR.
HARDWARE N/A Stocké dans la mémoire graphique (gralloc/DMABuf). Voir Bitmaps matériels.

Formule de mémoire : Memory (Bytes) = Width × Height × Bytes Per Pixel

Par exemple, une image plein écran sur un appareil 1080p (1920 x 1080) en ARGB_8888 prend : 1920 × 1080 × 4 octets ≈ 8,3 Mo.

Bitmaps de segment de mémoire et bitmaps partagés

Bitmaps de segment de mémoire (segment de mémoire natif)

Dans les versions modernes d'Android (8.0 ou ultérieures), les données de pixels bitmap sont stockées dans le segment de mémoire natif, tandis qu'un petit objet wrapper réside dans le segment de mémoire Java.

Lorsqu'une application doit présenter une image, elle est généralement décodée à partir d'un fichier image compressé dans un bitmap et stockée dans le tas de mémoire.

Bitmaps partagés (ashmem/memfd)

Lorsqu'un bitmap est transféré entre des processus (par exemple, via Binder vers SystemUI pour une notification), Android évite de copier les données de pixels en utilisant la mémoire partagée (ashmem ou memfd).

Une instance Bitmap peut être copiée explicitement dans la mémoire partagée en appelant Bitmap.asShared(), ou implicitement si un Bitmap est placé dans un Parcel (généralement en ajoutant le bitmap à un Parcelable tel qu'un Bundle) et envoyé via Binder IPC.

Lorsqu'un bitmap partagé est envoyé via Binder IPC, les données de pixels elles-mêmes ne sont pas copiées, mais un descripteur de fichier référençant une région de mémoire partagée est dupliqué dans le processus destinataire. La région de mémoire sous-jacente peut être partagée entre plusieurs processus et n'est pas libérée tant que tous les descripteurs de fichiers qui y font référence n'ont pas été fermés.

Bitmaps modifiables et immuables

  • Bitmaps modifiables : peuvent être modifiés après leur création (par exemple, via un Canvas). Ils nécessitent toujours leur propre allocation de mémoire privée. Si un bitmap modifiable est copié, une copie complète (deuxième copie de toutes les données de pixels) doit être effectuée.
  • Bitmaps immuables : ne peuvent pas être modifiés. Cela permet des optimisations telles que le partage du même tampon de mémoire sous-jacent entre différentes instances Bitmap. Les bitmaps chargés à partir de ressources APK (BitmapFactory) sont généralement immuables.

Gestion efficace des bitmaps

Mise en pool et réutilisation des bitmaps

L'allocation et la désallocation fréquentes de bitmaps entraînent une rotation de l'allocation, ce qui force le GC à s'exécuter en permanence. Les bibliothèques de chargement d'images courantes utilisent un pool de bitmaps.

Google recommande Glide comme solution pour les applications basées sur Java et Coil pour les applications basées sur Kotlin (en particulier lors de l'utilisation de Jetpack Compose).

Lorsqu'un bitmap n'est plus nécessaire, au lieu de le laisser être récupéré par le GC, l'application appelle bitmap.recycle() ou le renvoie à un pool. La prochaine fois qu'un bitmap des mêmes dimensions et de la même configuration sera nécessaire, le pool fournira le tampon existant, ce qui évitera une nouvelle allocation.

Bitmaps matériels

Bitmap.Config.HARDWARE vous permet de stocker directement les données de pixels dans la mémoire graphique (DMABuf).

  • Avantages:
    • Économies de mémoire : n'utilise pas le segment de mémoire d'application ni le segment de mémoire natif, mais la mémoire GPU. Souvent, les bitmaps affichés dans l'interface utilisateur d'une application doivent de toute façon être copiés dans la mémoire GPU. Cela permet donc d'économiser cette opération de copie et le coût de mémoire supplémentaire.
    • Performances : le dessin est extrêmement rapide, car les données se trouvent déjà sur le GPU.
  • Inconvénients:
    • Immuable : les bitmaps matériels ne peuvent pas être modifiés.
    • La lecture est lente : l'accès aux pixels depuis le processeur (par exemple, getPixel()) est très coûteux.
    • Attribution : plus difficile à suivre dans les outils standards tels qu'AHAT (voir ci-dessous).

Exercice pratique : exploration des bitmaps

Nous allons utiliser l'exemple d'application BitmapLab pour explorer ces concepts.

1. Mesure avec dumpsys meminfo

Lancez BitmapLab , puis appuyez sur ALLOCATE 10MB ARGB_8888. Exécutez ensuite la commande ci-dessous :

adb shell dumpsys meminfo -s com.android.bitmaplab

Dans les versions modernes d'Android, recherchez la section Native Allocations (Allocations natives). Elles offrent une bien meilleure attribution pour les bitmaps que le App Summary (Résumé de l'application) générique :

 Native Allocations
                         Count                       Total(kB)
                        ------                         ------
   Bitmap (malloced):        1                          10240  # <--- 10MB Bitmap data!
Bitmap (nonmalloced):        0                              0
  • Bitmap (malloced) : bitmaps alloués dans le segment de mémoire natif du processus. C'est là que la plupart des bitmaps standards résident dans Android 8.0 ou version ultérieure.
  • Bitmap (nonmalloced) : bitmaps qui utilisent une mémoire spécialisée telle que les bitmaps matériels ou les bitmaps partagés (via ashmem ou memfd).

Si vous allouez un bitmap partagé dans BitmapLab, il s'affiche dans Bitmap (nonmalloced) :

 Native Allocations
                         Count                       Total(kB)
                        ------                         ------
   Bitmap (malloced):        1                          10240
Bitmap (nonmalloced):        1                          10240  # <--- Shared Bitmap!

Suivi des bitmaps partagés

Sur certaines versions d'Android et configurations de noyau, dumpsys meminfo fournit également un suivi haute résolution des bitmaps mappés dans l'espace d'adressage du processus via des descripteurs de fichiers.

Par défaut, les bitmaps partagés utilisent un nom générique ("bitmap"). Pour activer l'attribution détaillée et le suivi unique des bitmaps (identification des bitmaps partagés dans différents processus), vous devez activer la propriété système suivante :

adb shell setprop debug.hwui.bitmap_ashmem_long_name true

Une fois activées, les régions ashmem de /proc/<pid>/smaps auront des noms plus descriptifs. meminfo en tirera parti, et les résultats se présenteront comme suit :

 Shared Bitmaps
                         Count                       Size(KB)
                        ------                         ------
              Mapped:        1                          10240
              Unique:        1                          10240
  • Mapped (Mappé) : taille totale de tous les mappages de mémoire liés aux bitmaps.
  • Unique (Unique) : taille des bitmaps en ne considérant que les éléments uniques (c'est-à-dire que deux mappages ou plus des mêmes données de pixels bitmap partagées sous-jacentes ne sont comptabilisés qu'une seule fois).

2. Bitmaps dans AHAT

AHAT offre une excellente visualisation des bitmaps.

  1. Dans BitmapLab, allouez quelques bitmaps.
  2. Capturez une empreinte de la mémoire avec l'option -b (pour inclure les données bitmap natives) :

    adb shell am dumpheap -b png com.android.bitmaplab /data/local/tmp/bitmaps.hprof
    adb pull /data/local/tmp/bitmaps.hprof .
    ahat bitmaps.hprof
    
  3. Ouvrez localhost:7100 et recherchez le lien Bitmaps dans la barre latérale ou la classe Bitmap.

  4. AHAT affichera les bitmaps dans le navigateur, ce qui vous permettra d'identifier facilement les images qui consomment de la mémoire.

AHAT affichant des bitmaps rendus

3. Pistes bitmap dans Perfetto

Perfetto peut suivre les allocations et les nombres de bitmaps au fil du temps. Ces compteurs sont émis par le framework Android lorsque la catégorie atrace gfx est activée pour une application spécifique.

  1. Démarrez une trace. Vous devez inclure la catégorie gfx et cibler le package d'application spécifique à l'aide de l'option -a :

    external/perfetto/tools/record_android_trace -o bitmaps.perfetto-trace \
        -t 15s -b 64mb view gfx dalvik am res memory -a com.android.bitmaplab
    
  2. Dans BitmapLab, appuyez plusieurs fois sur les boutons Allocate (Allouer) et Clear (Effacer).

  3. Appuyez également sur Parcel/Unparcel Bitmap (Bitmap de colis/déballage).

  4. Analysez la trace dans ui.perfetto.dev.

Dans la section de processus pour com.android.bitmaplab, vous verrez : * Nombre de bitmaps : compteur indiquant le nombre de bitmaps actifs. * Bitmap Memory (Mémoire bitmap) : compteur indiquant le nombre total d'octets utilisés par les bitmaps.

Tranches de haut niveau (SDK Perfetto)

BitmapLab utilise également le SDK Perfetto pour émettre des tranches de haut niveau pour les opérations bitmap. Recherchez BitmapLab_ dans la trace pour trouver : * BitmapLab_parcelUnparcel : tranches couvrant la logique de colisage et de déballage. * BitmapLab_postNotification : tranches couvrant le flux de publication des notifications.

Suivi des flux de notification

Lorsque vous appuyez sur Post Notification (Publier une notification), l'application crée une notification contenant le bitmap actuel et l'envoie au système. Le code du framework responsable de cette opération émet des tranches Perfetto avec des événements de flux qui connectent le colisage (écriture du bitmap dans un colis à envoyer via Binder IPC) et le déballage (lecture du bitmap à partir d'un colis à l'extrémité de réception).

Dans la capture d'écran ci-dessous, vous pouvez voir l'application qui emballe le grand bitmap à utiliser dans une transaction Binder pour publier la notification, ainsi que le déballage correspondant dans le processus system_server.

Perfetto montrant un flux de BitmapLab vers system_server via une notification

Avec Perfetto, vous pouvez même suivre le même bitmap de notification lorsqu'il se propage davantage entre les threads et les processus, par exemple d'un thread Binder dans system_server (qui implémente le serveur Binder INotificationManager) vers les threads de travail system_server qui peuvent ensuite transférer le même bitmap vers com.android.systemui pour qu'il s'affiche dans le volet des notifications.

Défis liés aux applications système

Les applications système telles que SystemUI (Notifications) et Launcher sont confrontées à des défis uniques :

  1. Contenu illimité : les notifications et les widgets peuvent être nombreux. Si chacun contient un grand bitmap, le système peut rapidement manquer de mémoire.
  2. Duplication : la même icône d'application peut être conservée dans le cache du lanceur, la zone de notification de SystemUI et l'application Paramètres.
  3. Partage via des tampons matériels : pour atténuer ce problème, les composants système évoluent vers un service centralisé de "déchargement d'images" qui partage les instances HardwareBuffer entre les processus.
  4. Attribution DMABuf : les bitmaps matériels économisent de l'espace de segment de mémoire, mais utilisent la mémoire DMABuf , qui est plus difficile à attribuer à un processus spécifique dans les outils de mémoire standards.

    Utilisez adb shell dmabuf_dump pour afficher les allocations DMABuf à l'échelle du système. Cet outil fournit une répartition des tampons par processus :

     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 : taille totale du tampon s'il est mappé dans le processus.
    • Pss : taille proportionnelle (RSS divisé par le nombre de processus partageant le tampon). Il s'agit de la meilleure métrique pour la comptabilité.
    • nr_procs : nombre de processus qui détiennent actuellement une référence à ce tampon.
    • Exporter : pilote qui a alloué le tampon (par exemple, virtio_gpu sur Cuttlefish ou un segment de mémoire Ion/DMA-BUF spécifique au fournisseur sur le matériel).

    Vous pouvez également utiliser adb shell dmabuf_dump -b pour obtenir un résumé de tous les tampons et de l'utilisation totale du DMA-BUF à l'échelle du système.


← Java | ↑ Haut | Natif →