Bitmaps e memória

Os objetos de bitmap são, com frequência, os maiores contribuintes individuais para o consumo de memória de um aplicativo. Sejam ícones de apps, imagens de notificação ou conteúdo de mídia, o processamento ineficiente de bitmaps pode levar rapidamente a erros de falta de memória (OOM, na sigla em inglês) e pressão de memória em todo o sistema.

Configurações de bitmap e dados de pixel

A quantidade de memória consumida por um bitmap é determinada principalmente pelas dimensões (largura × altura) e pela configuração (Bitmap.Config).

A configuração define quantos bytes são usados para representar cada pixel:

Configuração Bytes por pixel Descrição
ALPHA_8 1 Somente canal alfa (transparência). Útil para máscaras.
RGB_565 2 Vermelho (5 bits), verde (6 bits), azul (5 bits). Sem alfa. Bom para imagens opacas em que a alta fidelidade de cores não é essencial.
ARGB_8888 4 Alfa, vermelho, verde, azul (8 bits cada). Padrão e mais comum.
RGBA_F16 8 Ponto flutuante de meia precisão. Usado para conteúdo HDR e de gama ampla.
HARDWARE N/A Armazenado na memória gráfica (gralloc/DMABuf). Consulte Bitmaps de hardware.

Fórmula de memória:Memory (Bytes) = Width × Height × Bytes Per Pixel

Por exemplo, uma imagem de tela cheia em um dispositivo de 1080p (1920 x 1080) em ARGB_8888 ocupa: 1920 × 1080 × 4 bytes ≈ 8,3 MB.

Bitmaps de heap x bitmaps compartilhados

Bitmaps de heap (heap nativo)

No Android moderno (8.0 e mais recentes), os dados de pixel do bitmap são armazenados no heap nativo, enquanto apenas um pequeno objeto wrapper reside no heap Java.

Quando um app precisa apresentar uma imagem, ela geralmente é decodificada de um arquivo de imagem compactado em um bitmap e armazenada no heap.

Bitmaps compartilhados (ashmem/memfd)

Quando um bitmap é transferido entre processos (por exemplo, via Binder para SystemUI para uma notificação), o Android evita copiar os dados de pixel usando memória compartilhada (ashmem ou memfd).

Uma instância Bitmap pode ser copiada para a memória compartilhada explicitamente chamando Bitmap.asShared(), ou implicitamente se um Bitmap for colocado dentro de um Parcel (normalmente adicionando o Bitmap a um Parcelable, como um Bundle) e enviado pelo Binder IPC.

Quando um bitmap compartilhado é enviado pelo Binder IPC, os dados de pixel em si não são copiados, mas um descritor do arquivo que faz referência a uma região de memória compartilhada é duplicado para o processo do destinatário. A região de memória subjacente pode ser compartilhada entre vários processos e não é liberada até que todos os descritores de arquivo que fazem referência a ela sejam fechados.

Bitmaps mutáveis x imutáveis

  • Bitmaps mutáveis: podem ser modificados após a criação (por exemplo, usando um Canvas). Eles sempre exigem a própria alocação de memória privada. Se um bitmap mutável for copiado, uma cópia completa (segunda cópia de todos os dados de pixel) precisará ser feita.
  • Bitmaps imutáveis: não podem ser alterados. Isso permite otimizações, como compartilhar o mesmo buffer de memória subjacente entre diferentes instâncias Bitmap. Os bitmaps carregados de recursos de APK (BitmapFactory) geralmente são imutáveis.

Processamento eficiente de bitmaps

Pool e reutilização de bitmaps

A alocação e desalocação frequentes de bitmaps causam churn de alocação, o que força o GC a ser executado constantemente. As bibliotecas comuns de carregamento de imagens usam um pool de bitmaps.

O Google recomenda o Glide como solução para aplicativos baseados em Java e o Coil para aplicativos baseados em Kotlin (especialmente ao usar o Jetpack Compose).

Quando um bitmap não é mais necessário, em vez de deixá-lo ser coletado pelo GC, o app chama bitmap.recycle() ou o retorna a um pool. Na próxima vez que um bitmap das mesmas dimensões e configuração for necessário, o pool vai fornecer o buffer existente, evitando uma nova alocação.

Bitmaps de hardware

Bitmap.Config.HARDWARE permite armazenar dados de pixel diretamente na memória gráfica (DMABuf).

  • Prós:
    • Economia de memória: não usa o heap nativo ou do aplicativo; usa a memória da GPU. Muitas vezes, os bitmaps mostrados na interface de um app precisam ser copiados para a memória da GPU. Isso economiza a operação de cópia e o custo de memória adicional.
    • Performance: extremamente rápido de desenhar porque os dados já estão na GPU.
  • Contras:
    • Imutável: os bitmaps de hardware não podem ser modificados.
    • A leitura é lenta: o acesso a pixels da CPU (por exemplo, getPixel()) é muito caro.
    • Atribuição: mais difícil de rastrear em ferramentas padrão, como o AHAT (consulte abaixo).

Exercício prático: exploração de bitmap

Vamos usar o app de exemplo BitmapLab para explorar esses conceitos.

1. Medição com dumpsys meminfo

Inicie o BitmapLab e toque em ALLOCATE 10MB ARGB_8888. Depois execute:

adb shell dumpsys meminfo -s com.android.bitmaplab

Em versões modernas do Android, procure a seção Native Allocations. Elas oferecem uma atribuição muito melhor para bitmaps do que o App Summary genérico:

 Native Allocations
                         Count                       Total(kB)
                        ------                         ------
   Bitmap (malloced):        1                          10240  # <--- 10MB Bitmap data!
Bitmap (nonmalloced):        0                              0
  • Bitmap (malloced): bitmaps alocados no heap nativo do processo. É aqui que a maioria dos bitmaps padrão reside no Android 8.0 e mais recentes.
  • Bitmap (nonmalloced): bitmaps que usam memória especializada, como bitmaps de hardware ou bitmaps compartilhados (via ashmem ou memfd).

Se você alocar um bitmap compartilhado no BitmapLab, ele vai aparecer em Bitmap (nonmalloced):

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

Rastreamento de bitmaps compartilhados

Em algumas versões do Android e configurações de kernel, dumpsys meminfo também oferece rastreamento de alta resolução para bitmaps mapeados no espaço de endereço do processo usando descritores de arquivo.

Por padrão, os bitmaps compartilhados usam um nome genérico ("bitmap"). Para ativar a atribuição detalhada e o rastreamento de bitmap exclusivo (identificando bitmaps compartilhados em diferentes processos), ative a seguinte propriedade do sistema:

adb shell setprop debug.hwui.bitmap_ashmem_long_name true

Quando ativadas, as regiões ashmem em /proc/<pid>/smaps terão nomes mais descritivos. meminfo vai aproveitar isso, e os resultados serão assim:

 Shared Bitmaps
                         Count                       Size(KB)
                        ------                         ------
              Mapped:        1                          10240
              Unique:        1                          10240
  • Mapeado: o tamanho total de todos os mapeamentos de memória relacionados a bitmap.
  • Exclusivo: o tamanho dos bitmaps considerando apenas os exclusivos (ou seja, dois ou mais mapeamentos dos mesmos dados de pixel de bitmap compartilhado subjacente contam apenas uma vez).

2. Bitmaps no AHAT

O AHAT oferece uma excelente visualização para bitmaps.

  1. No BitmapLab, aloque alguns bitmaps.
  2. Capture um heap dump com a flag -b (para incluir dados de bitmap nativo):

    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. Abra localhost:7100 e procure o link Bitmaps na barra lateral ou pesquise a classe Bitmap.

  4. O AHAT vai renderizar os bitmaps no navegador, facilitando a identificação de quais imagens estão consumindo memória.

AHAT mostrando bitmaps renderizados

3. Faixas de bitmap no Perfetto

O Perfetto pode rastrear alocações e contagens de bitmap ao longo do tempo. Esses contadores são emitidos pelo framework do Android quando a categoria gfx atrace está ativada para um aplicativo específico.

  1. Inicie um rastreamento. Você precisa incluir a categoria gfx e segmentar o pacote do app específico usando a flag -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. No BitmapLab, toque nos botões Allocate e Clear repetidamente.

  3. Toque também em Parcel/Unparcel Bitmap.

  4. Analise o rastreamento em ui.perfetto.dev.

Na seção de processos para com.android.bitmaplab, você verá: * Bitmap Count: um contador que mostra o número de bitmaps ativos. * Memória de Bitmap: um contador que mostra o total de bytes usados por bitmaps.

Fatias de alto nível (SDK do Perfetto)

O BitmapLab também usa o SDK do Perfetto para emitir fatias de alto nível para operações de bitmap. Pesquise BitmapLab_ no rastreamento para encontrar: * BitmapLab_parcelUnparcel: fatias que abrangem a lógica de empacotamento e desempacotamento logic. * BitmapLab_postNotification: fatias que abrangem o fluxo de postagem de notificações.

Como rastrear fluxos de notificação

Quando você toca em Post Notification, o app cria uma notificação contendo o bitmap atual e a envia para o sistema. O código do framework responsável por isso emite fatias do Perfetto com eventos de fluxo que conectam o empacotamento (gravação do bitmap em um pacote a ser enviado pelo Binder IPC) e o desempacotamento (leitura do bitmap de um pacote na extremidade de recebimento).

Na captura de tela abaixo, é possível ver o app empacotando o bitmap grande a ser usado em uma transação do Binder para postar a notificação e o desempacotamento correspondente no processo system_server.

Perfetto mostrando um fluxo do BitmapLab para system_server via notificação

Usando o Perfetto, você pode até mesmo seguir o mesmo bitmap de notificação à medida que ele se propaga ainda mais em threads e processos, por exemplo, de uma thread do Binder em system_server (que implementa o servidor do Binder INotificationManager) para threads de trabalho system_server que podem encaminhar o mesmo bitmap para com.android.systemui para ser mostrado na sombra de notificações.

Desafios de apps do sistema

Apps do sistema, como SystemUI (notificações) e Launcher , enfrentam desafios exclusivos:

  1. Conteúdo ilimitado: as notificações e os widgets podem ser numerosos. Se cada um deles tiver um bitmap grande, o sistema poderá ficar sem memória rapidamente.
  2. Duplicação: o mesmo ícone do app pode ser mantido no cache do Launcher, na área de notificação do SystemUI e no app Configurações.
  3. Compartilhamento via buffers de hardware: para atenuar isso, os componentes do sistema estão migrando para um serviço centralizado de "descarregamento de imagens" que compartilha HardwareBuffer instâncias entre processos.
  4. Atribuição de DMABuf: os bitmaps de hardware economizam espaço de heap, mas usam a memória DMABuf , que é mais difícil de atribuir a um processo específico em ferramentas de memória padrão.

    Use adb shell dmabuf_dump para conferir as alocações de DMABuf em todo o sistema. Essa ferramenta fornece uma detalhamento de buffers por processo:

     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: o tamanho total do buffer se ele estiver mapeado no processo.
    • Pss: o tamanho proporcional (RSS dividido pelo número de processos que compartilham o buffer). Essa é a melhor métrica para contabilidade.
    • nr_procs: o número de processos que atualmente têm uma referência a esse buffer.
    • Exportador: O driver que alocou o buffer (por exemplo, virtio_gpu no Cuttlefish ou um heap Ion/DMA-BUF específico do fornecedor no hardware).

    Também é possível usar adb shell dmabuf_dump -b para um resumo de todos os buffers e o uso total de DMA-BUF em todo o sistema.


← Java | ↑ Up | Native →