Definir orçamentos de memória de apps

Os orçamentos de memória do app permitem que os apps declarem um orçamento de memória para si mesmos, o que informa ao sistema para reduzir o uso de memória quando o app usa mais do que o orçamento definido. Isso é útil principalmente para apps do sistema e em pacote ou para apps destinados a dispositivos com restrição de memória, em que o desenvolvedor conhece o conjunto de trabalho de memória esperado (a memória que o app precisa ativamente para o que o usuário está fazendo) e quer garantir que o app não use muitos dos recursos de RAM compartilhados do sistema.

O orçamento é mantido equilibrado usando a remoção e troca de memória para remover páginas de memória que não foram usadas recentemente, concentrando o consumo de memória do app no conjunto de trabalho atual. Quando um app excede o orçamento declarado, o sistema operacional direciona a recuperação especificamente para esse app:

  1. As páginas limpas com backup de arquivos (como código inativo e recursos mapeados) são removidas primeiro porque podem ser lidas novamente no armazenamento, se necessário.
  2. As páginas sujas com suporte de arquivo são gravadas de volta no armazenamento e removidas.
  3. As páginas de memória anônimas (como alocações de heap) são compactadas e trocadas para a zRAM.

Desde que o conjunto de trabalho não exceda o orçamento, o app vai funcionar bem sem usar mais memória do que o orçamento definido. O sistema operacional remove a memória não utilizada e compacta as páginas de heap inativas para troca, de modo que as alocações de memória permaneçam limitadas sem encerrar o processo.

O que acontece quando um app excede o orçamento

Exceder um orçamento de memória não faz com que o sistema encerre ou cause falhas no app. Em vez disso, quando um app usa mais memória do que o orçamento permite, o SO encontra memória que o app não usa há algum tempo e a armazena com segurança até que ela seja necessária novamente. Isso abre espaço para novas alocações de memória do app e mantém o uso geral de memória do app dentro do limite.

Enquanto houver espaço entre o que o app precisa em um determinado momento (o conjunto de trabalho) e o orçamento definido, o app vai continuar funcionando normalmente. Se um app definir um orçamento menor que o conjunto de trabalho, o desempenho do app no tempo de execução poderá ser reduzido.

Para saber como o SO rastreia e recupera memória, consulte o guia de arquitetura de memória:

Declarar orçamentos no manifesto do Android

Declarar os orçamentos de memória no AndroidManifest.xml é o método principal e recomendado para definir orçamentos. Ele não requer código de ambiente de execução, entra em vigor imediatamente após a inicialização do processo e fornece um contrato claro para o sistema operacional.

As declarações <memory-budget> entram em vigor em dispositivos com o Android 17 QPR2 (nível 37.2 da API) e versões mais recentes. Em versões anteriores do Android, o analisador de manifesto da plataforma ignora com segurança elementos XML não reconhecidos. Assim, você pode adotar <memory-budget> sem afetar a compatibilidade com versões anteriores.

Declarar um orçamento de referência

Para a maioria dos apps, basta definir um único orçamento para o aplicativo. Declare um elemento <memory-budget> diretamente dentro da tag <application>:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.simpleapp">

    <application
        android:label="@string/app_name">

        <!-- Baseline budget for the application -->
        <memory-budget android:maxMb="256" />

    </application>
</manifest>

Isso define um orçamento de memória residente de 256 MB em todos os processos e estados do pacote. Quando o consumo de memória do app excede 256 MB, o sistema operacional reduz as páginas de memória inativas usando remoção e troca.

Variar os orçamentos por estado do processo

Um app requer quantidades diferentes de memória, dependendo da visibilidade do usuário:

  • Primeiro plano: o processo está hospedando uma atividade visível que interage com o usuário. Esse estado normalmente tem a maior pegada devido à interface e aos gráficos ativos.
  • Perceptível: o processo é perceptível para o usuário, mas não hospeda uma janela visível (por exemplo, hospeda um serviço em primeiro plano de reprodução de mídia, um download em segundo plano ativo, navegação guiada ou um método de entrada ativo).
  • Segundo plano: o processo está executando jobs, receptores ou sincronizações de dados em segundo plano. Ele precisa manter uma pegada mínima.

É possível declarar várias cláusulas <memory-budget> para corresponder a estes estados:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.simpleapp">

    <application
        android:label="@string/app_name">

        <!-- Default budget for visible foreground UI -->
        <memory-budget android:maxMb="200" />

        <!-- Tighter budget when playing audio in background -->
        <memory-budget
            android:maxMb="120"
            android:state="perceptible" />

        <!-- Minimal budget when fully in background -->
        <memory-budget
            android:maxMb="48"
            android:state="background" />

    </application>
</manifest>

Uma cláusula base sem android:state não é obrigatória. Se você especificar apenas cláusulas específicas de estado (por exemplo, android:state="background"), os outros estados não serão limitados por um orçamento de app. Quando incluída, uma cláusula sem android:state atua como o substituto padrão para estados não especificados (como primeiro plano), que as cláusulas mais restritivas subsequentes substituem quando o app faz a transição para os estados perceptible ou background.

Ao dimensionar os orçamentos para cada estado, use como meta o conjunto de trabalho ativo desse estado, em vez do pico de memória residente (marca d'água alta do RSS):

  • Orçamentos de tamanho foreground para renderização de frames sem problemas: dê à interface em primeiro plano margem suficiente para o conjunto de trabalho ativo, de modo que a recuperação síncrona não atrase a renderização de frames. Se o app incluir um fluxo de trabalho independente que consome muita memória, como um editor de imagens ou vídeos de alta resolução, considere mover essa atividade para um processo dedicado (consulte Apps de vários processos) para que as alocações sejam orçadas de forma independente e removidas quando o usuário sair.
  • Defina orçamentos de tamanho background rigorosos para o trabalho headless: um RSS em segundo plano alto na telemetria de campo geralmente reflete alocações de interface retidas após sair do primeiro plano (consulte Colocar um serviço vinculado no mesmo local que uma interface com uso intensivo de memória e Liberar memória em resposta a eventos) ou leituras de arquivos únicas durante a indexação e as sincronizações em segundo plano. Como o trabalho em segundo plano sem interface não tem prazos de frame, remover páginas de cache de arquivos únicos e trocar páginas de heap inativas para zRAM quando uma sincronização em segundo plano aloca brevemente além do orçamento não causa instabilidade visível ao usuário, além de evitar que picos em segundo plano removam o app em primeiro plano do usuário.

Para um fluxo de trabalho detalhado, consulte Medir o conjunto de trabalho e calibrar orçamentos.

Como os estados de orçamento são mapeados para os estados de processamento

A plataforma mapeia os estados de orçamento do manifesto para os estados do processo de execução com base em RunningAppProcessInfo.importance, que o Activity Manager calcula com base nos componentes ativos do processo e em qualquer vinculação de serviço ou consulta de provedor de conteúdo recebida de outros apps:

  • foreground: interações ativas e visíveis do usuário, como hospedar uma atividade retomada, manter o estado do app principal quando a tela é desligada ou hospedar um serviço ou provedor de conteúdo vinculado ou consultado ativamente pelo app em primeiro plano.
  • perceptible: cargas de trabalho que são perceptíveis ao usuário sem uma janela visível, como reprodução de mídia ativa, navegação guiada, captura de câmera ou microfone, downloads em segundo plano ativos, serviços em primeiro plano de sincronização de dados ou serviços vinculados por um cliente em primeiro plano usando flags, como BIND_NOT_FOREGROUND (consulte Como controlar a herança com flags BIND). Embora as métricas internas da plataforma possam rotular algumas dessas cargas de trabalho como PROCESS_STATE_IMPORTANT_FOREGROUND, essa constante interna não significa uma janela visível e é regida pelo orçamento de perceptible.
  • background: trabalho que não é imediatamente perceptível para o usuário, como jobs em segundo plano, alarmes e broadcast receivers.

A tabela a seguir mostra como os níveis de importância do ambiente de execução são mapeados para os estados do manifesto:

Manifesto android:state Importância do ambiente de execução (RunningAppProcessInfo) Componentes típicos
foreground IMPORTANCE_FOREGROUND
IMPORTANCE_TOP_SLEEPING
Atividade visível retomada, app principal com tela bloqueada, serviço ou provedor de conteúdo vinculado ao app principal
perceptible IMPORTANCE_FOREGROUND_SERVICE
IMPORTANCE_VISIBLE
Serviços em primeiro plano de reprodução de mídia, navegação, downloads ou sincronização ativos; serviço vinculado a flags de serviço em primeiro plano ou visíveis
background IMPORTANCE_PERCEPTIBLE
IMPORTANCE_CANT_SAVE_STATE
IMPORTANCE_SERVICE
Jobs em segundo plano, receptores, sincronização em segundo plano

Para saber como as vinculações de cliente elevam um processo de host de serviço ou provedor e como o processo volta para background ou cached depois que o cliente se desvincula ou sai da tela, consulte Vinculações de serviço e estados de processo.

Quando um processo entra em um estado armazenado em cache (IMPORTANCE_CACHED) sem componentes ativos, a plataforma libera o orçamento de memória ativa e gerencia a memória usando a recuperação padrão de processos armazenados em cache.

Inspecionar o estado e o orçamento do processo do app

Uma maneira de inspecionar o estado e a importância do processo ativo do app durante o desenvolvimento é consultar o Activity Manager usando o ADB:

adb shell dumpsys activity processes <package-name>

O exemplo abreviado a seguir mostra o registro do processo e as entradas de controle de OOM para um app que executa um serviço em primeiro plano:

ACTIVITY MANAGER RUNNING PROCESSES (dumpsys activity processes)
  All known processes:
  *APP* UID 10123 ProcessRecord{edf056c 3919:com.example.app/u0a123}
    pid=3919
    oom adj: max=1001 curRaw=200 setRaw=200 cur=200 set=200
    curProcState=4 mRepProcState=4 setProcState=4 lastStateTime=-42s360ms
    hasStartedServices=true
    mHasForegroundServices=true forcingToImportant=null
...
  Process OOM control (48 total):
    Proc #22: prcp  F/S/FGS  ---NFU-TI  t: 0 3919:com.example.app/u0a123 (fg-service)
        oom: max=1001 curRaw=200 setRaw=200 cur=200 set=200
        state: cur=FGS  set=FGS  lastRss=0.00 lastCachedRss=0.00

Nesta saída:

  • curProcState=4 e state: cur=FGS refletem que o processo está no estado de serviço em primeiro plano. Se o app estiver hospedando uma atividade visível ativa, isso vai aparecer como TOP. Se estiver executando um componente importante em primeiro plano (como um download ou uma sincronização), ele vai aparecer como IMPF.
  • Na tabela de controle de OOM do processo, prcp indica que o processo é avaliado no nível de prioridade perceptível, que corresponde ao orçamento de perceptible.

Para inspecionar o orçamento de memória atualmente aplicado e o uso da memória residente:

adb shell dumpsys meminfo <package-name>

A partir do Android 17 QPR2, essa saída inclui uma seção Memory Budget que mostra o limite máximo ativo, a origem do limite (como AndroidManifest ou MemoryBudgetManager) e a memória residente atual.

Apps de vários processos

Se o aplicativo dividir o trabalho em vários processos, configure orçamentos de processo dedicados usando a tag <process> dentro de <processes>.

Por exemplo, considere um app de streaming de música (com.example.radio):

  1. Processo principal: hospeda a interface visível e o mecanismo de reprodução de áudio (MediaSessionService com um serviço em primeiro plano mediaPlayback). Quando visível, o processo opera com o orçamento de primeiro plano de 180 MB. Quando o usuário sai do app enquanto a música continua tocando, o processo entra no estado perceptible, em que um orçamento de 64 MB é suficiente para o mecanismo de reprodução e o buffer de áudio.
  2. Processo de sincronização (:sync): processo dedicado que executa a sincronização de metadados em segundo plano e a indexação de download. Como esse processo só fica ativo em segundo plano, não é necessário declarar explicitamente state="background". Um único orçamento é aplicado.
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.radio">

    <application
        android:label="@string/app_name">

        <!-- Package baseline: main process with UI and audio playback -->
        <memory-budget android:maxMb="180" />

        <!-- Tighter budget when audio plays in the background -->
        <memory-budget
            android:maxMb="64"
            android:state="perceptible" />

        <processes>
            <!-- Dedicated background sync process -->
            <process android:process=":sync">
                <memory-budget android:maxMb="32" />
            </process>
        </processes>

        <service
            android:name=".playback.AudioPlayerService"
            android:foregroundServiceType="mediaPlayback"
            android:exported="false" />

        <service
            android:name=".sync.PlaylistSyncService"
            android:process=":sync"
            android:exported="false" />

    </application>
</manifest>

Ao declarar um bloco <processes> no manifesto, liste as entradas <process> apenas para os processos que declaram um <memory-budget>. Se você quiser orçar apenas subprocessos específicos e deixar o restante do app no limite do sistema para o dispositivo, omita os elementos <memory-budget> diretamente em <application>.

O uso de memória em qualquer subprocesso é contabilizado no orçamento do processo e no orçamento do pacote que o contém. Um processo sofre pressão de memória no momento da execução se violar o orçamento do processo ou do pacote, o limite que for atingido primeiro.

Ajustar orçamentos para telas de alta densidade

Para aplicativos em que o consumo de memória aumenta significativamente com o número de pixels que precisam ser desenhados na tela de uma só vez, como um app de galeria de fotos que armazena em cache bitmaps dimensionados para a tela, o Android oferece dois mecanismos alternativos para dimensionar os orçamentos de forma dinâmica com as especificações de exibição:

  • Escala por bucket de densidade de tela (android:additionalMbPerDensity): adiciona megabytes proporcionalmente ao quadrado da proporção de densidade da tela em relação a mdpi (1,0x / 160 dpi), porque os drawables rasterizados em 2D e os recursos de interface são dimensionados em largura e altura:

    <!-- Baseline 180MB + 16MB multiplied by (density ratio)^2 -->
    <memory-budget
        android:maxMb="180"
        android:additionalMbPerDensity="16" />
    

    Em uma tela mdpi (1,0x), o orçamento é 180 + 16 × 12 = 196 MB. Em uma tela xxhdpi (3.0x), o orçamento é dimensionado para 180 + 16 × 32 = 324 MB.

  • Escala por resolução de tela física (android:additionalBytesPerDisplayPixel): adiciona bytes diretamente por pixel de tela física (largura × altura). Isso é ideal para aplicativos que alocam superfícies gráficas em tela cheia, buffers de renderização ou caches de fotos em resolução máxima, em que o consumo de memória é dimensionado diretamente com a contagem de pixels de exibição brutos, em vez de buckets de densidade da interface:

    <!-- Baseline 128MB + 16 bytes per physical display pixel -->
    <!-- For example, a 4-byte RGBA full-screen buffer with double or quadruple buffering -->
    <memory-budget
        android:maxMb="128"
        android:additionalBytesPerDisplayPixel="16" />
    

    Em uma tela de 1080p (1080 × 2400 &approx; 2,59 milhões de pixels), isso adiciona &approx; 41,4 MB ao orçamento de referência. Em uma tela de 1440p (1440 × 3120 &approx; 4,49 milhões de pixels), ele adiciona &approx; 71,8 MB.

Esses dois atributos são alternativos. Escolha o atributo que corresponde ao fator de escalonamento principal do app e evite combinar os dois na mesma cláusula.

Especialização para formatos de dispositivo

Ao enviar um APK para smartphones, tablets e Wear OS, use o atributo android:feature para ajustar os orçamentos para diferentes destinos de hardware.

Em relógios Wear OS, a RAM é limitada, e a interface e o conjunto de recursos do app são muito mais simples. É possível declarar um orçamento mais restrito especializado para o recurso watch:

<!-- General phone and tablet baseline -->
<memory-budget android:maxMb="180" />

<!-- Wear OS override: simpler UI and constrained hardware -->
<memory-budget
    android:maxMb="48"
    android:feature="watch" />

Regra de resolução: a última cláusula aplicável entra em vigor

Ao definir vários elementos <memory-budget> para um aplicativo ou processo, o sistema os avalia na ordem em que são declarados no manifesto. A última cláusula de orçamento aplicável é a que será aplicada.

Como o último orçamento aplicável vence, a ordem é importante. Sempre coloque o orçamento de referência mais geral primeiro, seguido por substituições mais específicas (como cláusulas específicas de estado ou de hardware).

Referência de atributos XML

Todos os atributos de tamanho de memória são expressos em megabytes (MB) e são mapeados para a carga memory.current do cgroup do Linux (que exclui a memória compartilhada, como o Zygote).

Atributo Formato Padrão Descrição
android:maxMb Número inteiro (> 0) Obrigatório O limite de orçamento de memória residente de referência em MB.
android:state Tipo enumerado Qualquer O estado do processo ao qual esse orçamento se aplica: foreground, perceptible ou background. Se omitida, a cláusula vai funcionar como um substituto para qualquer estado não especificado.
android:additionalMbPerDensity Número inteiro (≥ 0) 0 Megabytes adicionais multiplicados pelo quadrado da proporção de densidade da tela em relação a mdpi (1,0x).
android:additionalBytesPerDisplayPixel Número inteiro (≥ 0) 0 Bytes adicionais alocados por pixel de tela física (largura × altura), úteis para buffers de superfície e bitmaps.
android:feature String Qualquer Restringe a cláusula a dispositivos que declaram recursos de hardware específicos: watch, automotive ou leanback.

Medir o conjunto de trabalho e calibrar os orçamentos

Um bom orçamento fica um pouco acima do conjunto de trabalho do app para um determinado estado, não no pico sem restrições. Para encontrar esse ponto, execute um fluxo do usuário realista, reduza o orçamento gradualmente e observe o momento em que o app começa a pagar por isso.

Entender o conjunto de trabalho em comparação com a memória residente

Sem um orçamento, o consumo residente do app (memory.current) contém dois tipos de memória:

  • O conjunto de trabalho: o heap Java, Kotlin e nativo e as páginas quentes com suporte de arquivos (código DEX, bibliotecas nativas e recursos) que o fluxo do usuário atual acessa repetidamente.
  • Folga fria: caches de bitmap de telas anteriores, buffers de inicialização única, folga de heap restante após uma intermitência de alocação e páginas de arquivo que foram lidas uma vez e nunca mais foram tocadas.

Um orçamento no pico sem restrições nunca recupera a folga fria. Um orçamento abaixo do conjunto de trabalho faz com que o kernel troque as páginas de heap e remova as páginas de código que o app imediatamente falha ao retornar. Essas falhas de conjunto de trabalho custam tempo de CPU e bateria e, quando chegam à linha de execução de interface, também custam frames.

O que você está procurando é o joelho: o menor orçamento em que a recuperação remove apenas a folga fria, de modo que as falhas de referência, o tempo de CPU e os tempos para a renderização do frame permaneçam onde estavam sem um orçamento.

Preparar o ambiente de teste

Configure um teste repetível que se comporte como a produção:

  • Calibre o mesmo build de lançamento otimizado para R8 e não depurável que você envia. As builds de depuração usam mais memória, então um orçamento ajustado em uma build de depuração é alto demais para a versão.
  • Faça testes em um dispositivo ou emulador com o Android 17 QPR2 (nível 37.2 da API) ou mais recente. Um build userdebug ou uma imagem de emulador sem o Google Play é melhor, porque alguns dispositivos só permitem ler os arquivos cgroup nas seções a seguir depois de executar adb root.
  • Crie um script de um fluxo do usuário de 30 a 60 segundos com o Jetpack Macrobenchmark, o UI Automator ou o adb shell input, uma vez para cada estado que você planeja incluir no orçamento. Um app inativo não informa nada sobre o conjunto de trabalho.
    • foreground: rolar listas densas e grades de mídia, mover-se entre os principais destinos, abrir menus e caixas de diálogo e executar animações.
    • perceptible: saia da janela visível enquanto o trabalho perceptível continua (por exemplo, reprodução de áudio ou download), deixe-o em execução por 10 a 15 segundos e volte para o app.
    • background: acionar uma sincronização, um job WorkManager (por exemplo, com adb shell cmd jobscheduler run -f <package-name> <job-id>), um serviço iniciado ou um broadcast receiver enquanto o app não tem uma interface visível.

Meça um orçamento de background enquanto o trabalho em segundo plano estiver em execução. Depois que o processo é armazenado em cache, a plataforma libera o orçamento (consulte Como os estados de orçamento são mapeados para os estados de processo). Portanto, um processo em cache ocioso não informa nada sobre o orçamento de background.

Defina seu valor de referência

Comece com execuções que não têm orçamento para saber como é o normal. Use um build sem declarações <memory-budget> e inicie cada execução em um novo processo (adb shell am force-stop <package-name>).

Cada processo de app tem o próprio controlador de memória do cgroup v2. Procure o caminho na linha 0:: de /proc/<PID>/cgroup em vez de criar o caminho por conta própria, porque o layout do cgroup pode ser diferente entre os dispositivos. Transmita cada comando do dispositivo para adb shell como uma única string entre aspas para que o shell do dispositivo veja as aspas:

PID=$(adb shell pidof -s <package-name>)
CG=/sys/fs/cgroup$(adb shell "sed -n 's/^0:://p' /proc/$PID/cgroup")
adb shell "cat $CG/memory.current $CG/memory.swap.current"
adb shell "grep -E '^(anon|file|workingset_refault_.*) ' $CG/memory.stat"

A leitura desses arquivos é barata e não interfere no app. Portanto, é possível fazer uma amostragem deles a cada segundo durante uma execução:

  • memory.current: memória residente cobrada do processo, em bytes. Esse é o número que o <memory-budget> limita.
  • memory.swap.current: memória compactada na troca de zRAM, em bytes.
  • anon e file em memory.stat: memória anônima residente e com suporte de arquivo, em bytes
  • workingset_refault_anon e workingset_refault_file em memory.stat: contagens em execução de páginas trocadas ou removidas que o app precisou novamente. Esses contadores são o principal sinal de que um orçamento está muito apertado.

Se você receber um erro de permissão, execute adb root e tente novamente. Se você não conseguir acesso raiz, o adb shell dumpsys meminfo <package-name> vai oferecer uma visualização mais ampla. Em App Summary, Java Heap, Native Heap e Stack são, em grande parte, memória anônima, e Code é apoiada por arquivos. Os bitmaps de software são contabilizados em Native Heap e no seu orçamento, enquanto os bitmaps de hardware e Graphics não são. Não execute dumpsys meminfo em um loop enquanto mede o tempo para a renderização do frame. Ele força uma coleta de lixo no app e percorre todos os mapeamentos de memória, o que muda o que você está tentando medir.

Analisar os orçamentos candidatos

É possível testar orçamentos candidatos sem recriar o app. Use am memory-limiter manual para definir um orçamento para um processo em execução e none para removê-lo:

adb shell am memory-limiter manual <PID> 120MB
adb shell am memory-limiter manual <PID> none

Uma substituição manual dura por todo o ciclo de vida da instância do processo, em todas as mudanças de estado do processo. Portanto, limpe-a ou reinicie o processo antes de continuar. Ele também define o limite rígido do processo (memory.max) com o mesmo valor. Assim, um orçamento candidato abaixo do conjunto de trabalho do app pode encerrar o processo, em vez de apenas deixá-lo mais lento. Trate isso como um orçamento com falha.

Também é possível fazer uma varredura em um build de comparação lendo um valor candidato de um argumento de instrumentação e definindo MemoryBudgetManager.processBudgetBytes. Um orçamento de ambiente de execução só pode restringir um orçamento de manifesto. Portanto, faça uma varredura em um build sem declarações <memory-budget>. Ele também permanece em vigor em todas as mudanças de estado do processo até que você chame clearProcessBudget.

Comece no memory.current mais alto do valor de referência e diminua de 10 a 15 MB por vez. Para cada orçamento candidato:

  1. Inicie um novo processo, direcione-o para o estado de destino e aplique o orçamento.
  2. Execute o fluxo do usuário uma vez como aquecimento. A aplicação de um orçamento remove as páginas de inicialização a frio uma vez, e você quer medir o estado estável em vez dessa transição.
  3. Redefina as estatísticas de frame e registre os contadores de refault iniciais e o tempo de CPU do processo (utime e stime, em tiques de relógio):

    adb shell dumpsys gfxinfo <package-name> reset
    adb shell "grep -E '^workingset_refault_(anon|file) ' $CG/memory.stat"
    adb shell "cut -d ' ' -f 14,15 /proc/$PID/stat"
  4. Execute a passagem medida do fluxo do usuário.

  5. Registre memory.current, anon, file, os contadores de falhas repetidas e o tempo de CPU novamente e colete estatísticas de frames. O dumpsys gfxinfo pode mostrar as mesmas estatísticas mais de uma vez. Portanto, use a primeira correspondência, que abrange todo o app:

    adb shell "dumpsys gfxinfo <package-name>" | grep -m 1 'Janky frames:'
    adb shell "dumpsys gfxinfo <package-name>" | grep -m 1 '95th percentile'

Repita cada candidato pelo menos três vezes e intercale os candidatos em vez de executar todas as repetições de um candidato consecutivamente. Compare cada resultado com a distribuição das execuções de referência, não com uma única execução.

Ler os resultados

À medida que o orçamento diminui, o app passa por três regimes:

  • Acima do joelho: o memory.current segue o orçamento descartando páginas frias de file e heap inativo. anon permanece estável, as falhas de referência permanecem próximas à referência, e o tempo de CPU e os tempos para a renderização do frame não se movem. É aqui que você quer estar.
  • Abaixo do joelho: as páginas frias desapareceram, então o kernel começa a trocar as páginas de heap que o app ainda usa. As falhas de página saltam em centenas ou milhares de páginas por execução, e o tempo de CPU aumenta. Os tempos para a renderização do frame podem quase não mudar em um dispositivo de teste inativo, mas o app agora gasta tempo de CPU e bateria compactando e descompactando o próprio conjunto de trabalho.
  • Bem abaixo do joelho: as falhas de recuperação e troca de entrada chegam à linha de execução de interface e ao RenderThread, e os tempos de instabilidade, tempo para a renderização do frame e de retomada rápida aumentam.

Em comparação com o valor de referência irrestrito, um orçamento candidato será aprovado se atender a todos os seguintes critérios, que são um ponto de partida razoável:

  • O processo sobrevive à execução
  • Os deltas de refault permanecem próximos à linha de base
  • O tempo de CPU do processo não aumenta mais de 5%
  • Os frames instáveis aumentam em no máximo 1 ponto percentual
  • O tempo para a renderização do frame P95 não aumenta mais do que 3 ms.
  • O tempo de retomada de aquecimento (TotalTime de adb shell am start -W) aumenta em no máximo 15% para os orçamentos perceptible e background

O ponto de inflexão é o menor orçamento candidato que passa. Por exemplo, veja uma varredura em primeiro plano para um app hipotético com um pico sem restrições de 138 MB:

Orçamento do candidato memory.current anon file Refaults (páginas) Tempo para a renderização do frame P95 Regime
Nenhum 138 MB 75 MB 63 MB 0 10,5 ms Valor de referência
125 MB 121 MB 74 MB 47 MB 10 10,5 ms Acima do joelho
110 MB 110 MB 75 MB 35 MB 15 10,8 ms Joelho
95 MB 95 MB 74 MB 21 MB 176 11,2 ms Abaixo do joelho
85 MB 85 MB 68 MB 17 MB 2.050 12,8 ms Abaixo do joelho
70 MB 70 MB 54 MB 16 MB 8.771 16,5 ms Bem abaixo do joelho

Escolha seu orçamento

Defina android:maxMb de 10% a 20% acima do limite, para que as diferenças nos dados do usuário, nas localidades e nos picos de alocação não empurrem o app para baixo dele. No exemplo anterior, o ponto de inflexão está em 110 MB, então 125 MB é uma boa escolha.

Em seguida, instale um build de lançamento com as novas declarações <memory-budget>, execute o fluxo do usuário novamente e verifique se os critérios de aprovação ainda são válidos. O dumpsys meminfo mostra o orçamento ativo e a origem dele na seção "Orçamento de memória".

Validar orçamentos que não estão em primeiro plano com um retorno rápido

Um orçamento apertado de perceptible ou background pode parecer bom enquanto o app está nesse estado, mas pode custar caro quando o usuário voltar. Teste também a viagem de volta:

  1. Execute o fluxo de primeiro plano, mova o app para o estado de destino e aplique o orçamento candidato com am memory-limiter manual.
  2. Deixe o trabalho perceptível ou em segundo plano ser executado por 10 a 15 segundos.
  3. Limpe a substituição (adb shell am memory-limiter manual <PID> none) e, em seguida, retorne imediatamente o app com adb shell am start -W e registre TotalTime. Se você definir o candidato usando MemoryBudgetManager, chame clearProcessBudget antes que o app retorne.
  4. Execute uma interação curta em primeiro plano e verifique as falhas de referência e as estatísticas de frame em relação aos critérios de aprovação.

Escolha o menor orçamento que passe em ambos os testes enquanto o app estiver nesse estado e no retorno de inicialização média, e adicione a mesma margem de 10% a 20%.

Calibrar o dimensionamento da tela

Se você usa android:additionalBytesPerDisplayPixel, encontre o ponto de inflexão do primeiro plano em duas telas com resoluções diferentes, como 1080 × 2400 (&approx; 2,59 milhões de pixels) e 1440 × 3120 (&approx; 4,49 milhões de pixels). Divida a diferença entre os dois joelhos, em bytes, pela diferença na contagem de pixels para receber android:additionalBytesPerDisplayPixel. Subtraia esse valor por pixel da tela menor do ponto de inflexão para conseguir a base independente de resolução e adicione margem para chegar a android:maxMb.

APIs de ambiente de execução (opção dinâmica secundária)

Declarar orçamentos de forma estática em AndroidManifest.xml é a solução preferida para quase todos os apps. No entanto, para aplicativos com cargas de trabalho dinâmicas ou para experimentos de ambiente de execução, o Android oferece APIs de SDK e NDK de ambiente de execução como uma opção secundária.

A API de ambiente de execução permite:

  • Consultar o uso da memória atual e os orçamentos efetivos.
  • Ajuste dinamicamente o orçamento do processo para baixo.
  • Detecte eventos de excesso de orçamento para reduzir os caches de forma proativa antes que o sistema operacional acione a recuperação direta.

API do SDK do Android (MemoryBudgetManager)

O serviço do sistema MemoryBudgetManager está disponível para apps escritos em Kotlin e Java a partir do Android 17 QPR2 (versão secundária do SDK, nível da API 37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2).

Recuperar o serviço

Antes de acessar MemoryBudgetManager, verifique se o dispositivo executa o Android 17 QPR2 ou mais recente em SDK_INT_FULL. Se o minSdk do app for anterior à 36, verifique primeiro SDK_INT para que os dispositivos com o Android 15 (nível 35 da API) ou versões anteriores não gerem NoSuchFieldError ao ler SDK_INT_FULL:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA &&
    Build.VERSION.SDK_INT_FULL >= Build.VERSION_CODES_FULL.CINNAMON_BUN_2
) {
    val budgetManager = context.getSystemService(MemoryBudgetManager::class.java)
}

Consultar uso e orçamentos

// Query current memory charged to this process and the package UID
val processUsageBytes = budgetManager.processCurrentUsageBytes
val packageUsageBytes = budgetManager.packageCurrentUsageBytes

// Query effective budgets (returns LIMIT_IS_DISABLED if unconstrained)
val processBudgetBytes = budgetManager.processBudgetBytes
val packageBudgetBytes = budgetManager.packageBudgetBytes

Definir ou limpar orçamentos de forma dinâmica

É possível definir um orçamento mais restrito no ambiente de execução para limitar a memória durante tarefas leves ou limpá-lo quando a tarefa for concluída:

// Set a tighter dynamic budget on the current process (e.g., 96 MB)
try {
    budgetManager.processBudgetBytes = 96L * 1024L * 1024L
} catch (e: IllegalArgumentException) {
    // Thrown if the budget is <= 0 or exceeds the manifest ceiling or system limit
    Log.e(TAG, "Requested budget exceeds manifest or system ceiling", e)
}

// Clear the dynamic process budget to restore the manifest limit (or unconstrained baseline)
budgetManager.clearProcessBudget()

Ao contrário das cláusulas de manifesto <memory-budget>, que trocam automaticamente os orçamentos sempre que o processo faz a transição entre os estados foreground, perceptible e background, um orçamento dinâmico definido por MemoryBudgetManager aplica um único limite até que o código seja atualizado ou limpo. Se você usa a API de ambiente de execução para realizar experimentos de campo ou gerenciar limites específicos de estado, atualize ou limpe o orçamento dinâmico em transições de ciclo de vida (por exemplo, usando ProcessLifecycleOwner ou callbacks de ciclo de vida da atividade, como mostrado no exemplo de editor de imagens adaptável).

Desempenho e idempotência

Ao trabalhar com MemoryBudgetManager no ambiente de execução, tenha em mente as seguintes características:

  • Idempotência: todas as APIs de mutação de orçamento são idempotentes. Chamar clearProcessBudget() ou clearPackageBudget() repetidamente ou quando nenhum orçamento dinâmico está ativo é uma operação segura. Da mesma forma, definir setProcessBudgetBytes() ou setPackageBudgetBytes() com o mesmo valor consecutivamente não aciona reconfigurações redundantes do sistema.
  • Frequência de chamada: definir, atualizar ou limpar um orçamento faz uma chamada entre processos para o serviço do sistema para atualizar o controlador de memória do sistema operacional para o processo. Embora seja leve, ele não é totalmente sem custo financeiro. Chame essas APIs durante transições de ciclo de vida discretas e marcos de tarefas, como entrar ou sair de uma atividade, iniciar ou concluir o processamento em segundo plano ou processar solicitações de fala. Evite chamá-las em loops apertados, linhas de execução de processamento de áudio ou rotinas de renderização por frame.
  • Frequência de consulta: as propriedades de consulta processCurrentUsageBytes e packageCurrentUsageBytes buscam o uso da memória em tempo real do serviço do sistema. Embora sejam rápidas, as consultas precisam ser invocadas sob demanda (como durante transições de tarefas ou dentro de callbacks OnOverBudgetListener), em vez de serem pesquisadas em loops contínuos.

Detectar callbacks de pressão de orçamento excedido

Os apps podem registrar um listener para receber notificações quando o uso da memória exceder o limite de orçamento. Isso permite que o app realize uma limpeza proativa no nível do app (como limpar caches de bitmap na memória) enquanto o sistema operacional recupera memória para manter o processo ou pacote dentro do orçamento:

val cleanupThread = HandlerThread("MemoryBudgetCleanup").apply { start() }

val listener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
    Log.w(TAG, "Process exceeded memory budget of $budgetBytes bytes")
    // Proactively evict thread-safe caches on the background Looper
    imageTileCache.evictAll()
}

// Register on a background Looper before tightening a dynamic budget
budgetManager.registerProcessOverBudgetListener(cleanupThread.looper, listener)

// When done (for example, in onStop)
budgetManager.unregisterProcessOverBudgetListener(listener)

Práticas recomendadas para callbacks de orçamento excedido:

  • Usar um plano de fundo Looper: transmita um plano de fundo HandlerThread Looper para registerProcessOverBudgetListener ou registerPackageOverBudgetListener sempre que possível. A execução da remoção de cache ou da limpeza de arquivos em uma linha de execução em segundo plano evita a disputa com a renderização de frames na linha de execução de interface enquanto o processo está sob pressão de memória. Reserve Looper.getMainLooper para listeners que só tocam no estado da interface vinculado à linha de execução principal.
  • Espere callbacks repetidos: os callbacks de orçamento excedido não são notificações únicas. Quando o uso excede o orçamento, o sistema operacional recupera as páginas frias primeiro. Se a pressão continuar, o sistema vai continuar recuperando memória e invocando o listener no máximo uma vez por segundo. Torne o callback idempotente. Se o app precisar de apenas uma notificação ou quiser um período de espera mais longo entre as passagens de limpeza, chame unregisterProcessOverBudgetListener (ou unregisterPackageOverBudgetListener) dentro do callback ou proteja a limpeza com um carimbo de data/hora de espera ou AtomicBoolean.
  • Registre-se antes de reduzir um orçamento: se o uso atual da memória já exceder um novo orçamento quando você chamar setProcessBudgetBytes ou setPackageBudgetBytes, um evento de orçamento excedido será acionado assim que o novo orçamento for aplicado. Registre o listener antes de definir o orçamento para que o app não perca essa notificação inicial de orçamento excedido.
  • Seja rápido: as operações de recuperação precisam oferecer alívio imediato. Cálculos complexos durante a pressão pioram o desempenho.
  • Evite alocações: não aloque novos objetos nem inicie novas linhas de execução no callback, porque isso pode acionar a recuperação direta imediata do sistema operacional.
  • Foque em metas de alto rendimento: remover bitmaps grandes, renderizar buffers ou fechar arquivos mapeados em memória é muito mais eficaz do que liberar muitos objetos pequenos.

API NDK nativa (<android/memory_budget_manager.h>)

Os apps nativos podem usar a API C NDK exposta por libandroid.so a partir do Android 17 QPR2 (nível 37.2 da API).

Configuração do CMake

find_library(android-lib android)
target_link_libraries(my_native_engine PRIVATE ${android-lib})

Incluir uso de cabeçalho e consulta

#include <android/memory_budget_manager.h>

// Query current memory usage
int64_t process_usage = AMemoryBudgetManager_getProcessCurrentUsageBytes();
int64_t package_usage = AMemoryBudgetManager_getPackageCurrentUsageBytes();

// Query current budget
int64_t process_budget = 0;
AMemoryBudgetResult result = AMemoryBudgetManager_getProcessBudget(&process_budget);
if (result == AMEMORY_BUDGET_RESULT_SUCCESS) {
    // Current budget available in process_budget
} else if (result == AMEMORY_BUDGET_RESULT_LIMIT_IS_DISABLED) {
    // No budget is currently active
}

Configurar dinamicamente o orçamento nativo

// Set a tighter process budget (e.g. 160MB)
AMemoryBudgetResult result = AMemoryBudgetManager_setProcessBudget(160LL * 1024 * 1024);
if (result != AMEMORY_BUDGET_RESULT_SUCCESS) {
    const char* error_msg = AMemoryBudgetManager_resultToString(result);
    // Handle error (e.g. AMEMORY_BUDGET_RESULT_ERROR_EXCEEDS_MANIFEST_LIMIT)
}

// Clear the dynamic budget to resume manifest limits (or unconstrained baseline)
AMemoryBudgetManager_clearProcessBudget();

Monitorar eventos de pressão de memória

O NDK oferece duas maneiras de monitorar eventos de memória:

  1. Observador de alto nível (AMemoryBudgetManager_Watcher_create): monitora eventos em um ALooper com debounce automático.
  2. Descritor de arquivo de baixo nível: AMemoryBudgetManager_getProcessMemoryPressureFd retorna um descritor do arquivo nativo que pode ser integrado diretamente a um loop de mecanismo epoll personalizado.
void onMemoryPressure(int32_t event_mask, const AMemoryBudgetEvents* events, void* userdata) {
    // High-yield eviction of unused native textures or geometry caches
    purgeNativeTextureCaches();
}

// Register watcher on a background worker ALooper with a 1000ms debounce interval
AMemoryBudgetManagerWatcher* watcher = AMemoryBudgetManager_Watcher_create(
    background_looper,
    AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
    1000 /* debounce_ms */,
    &onMemoryPressure,
    NULL /* userdata */
);

// When done:
AMemoryBudgetManager_Watcher_destroy(watcher);

Vincule o observador ao ALooper de uma linha de execução de worker em segundo plano, em vez da linha de execução de renderização ou da interface principal, para que a remoção do cache não entre em conflito com a renderização de frames. Crie o observador antes de restringir um orçamento com AMemoryBudgetManager_setProcessBudget ou AMemoryBudgetManager_setPackageBudget para que o app não perca o evento inicial de orçamento excedido. Enquanto o processo ou pacote permanecer acima do orçamento e o sistema operacional continuar recuperando memória, o observador vai invocar o callback repetidamente (no máximo uma vez por intervalo de debounce_ms). Torne o callback idempotente ou chame AMemoryBudgetManager_Watcher_destroy (que é seguro para chamar de dentro do callback) se o app precisar apenas de uma notificação única.

Exemplos da API Runtime

Os exemplos a seguir demonstram como implementar as APIs de ambiente de execução usando a API do SDK do Android (escrita em Kotlin) e a API NDK nativa (escrita em C++).

Exemplo do SDK do Android: editor de imagens adaptável

Este exemplo mostra um app de edição de imagens (com.example.imageeditor) em que o manifesto declara um limite de 256 MB para acomodar a tela de várias camadas:

<manifest ... >
    <application ... >
        <!-- Manifest ceiling accommodates the heaviest editing workload -->
        <memory-budget android:maxMb="256" />
    </application>
</manifest>

Quando o usuário está navegando pela galeria de miniaturas, o app usa a API do SDK do Android em Kotlin para reduzir dinamicamente o orçamento do processo para 96 MB. Quando o usuário abre a tela de edição de várias camadas, o app limpa o orçamento dinâmico para restaurar o limite total de 256 MB do manifesto. Ele também registra um OnOverBudgetListener em um HandlerThread Looper em segundo plano antes de aplicar o orçamento da galeria para remover bitmaps de visualização em cache sob pressão sem bloquear a linha de execução de interface.

package com.example.imageeditor.ui

import android.app.Activity
import android.app.MemoryBudgetManager
import android.graphics.Bitmap
import android.os.Bundle
import android.os.HandlerThread
import android.util.Log
import android.util.LruCache

class ImageEditorActivity : Activity() {

    private lateinit var budgetManager: MemoryBudgetManager
    private lateinit var cleanupThread: HandlerThread

    // Thread-safe in-memory cache for rendered preview tiles (32MB limit)
    private val previewCache = object : LruCache<String, Bitmap>(32 * 1024 * 1024) {
        override fun sizeOf(key: String, value: Bitmap): Int = value.byteCount
    }

    private val overBudgetListener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
        Log.w(TAG, "Process memory pressure detected (budget: ${budgetBytes / 1048576}MB). Evicting preview cache.")
        previewCache.evictAll()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        budgetManager = getSystemService(MemoryBudgetManager::class.java)
        cleanupThread = HandlerThread("MemoryBudgetCleanup").apply { start() }
    }

    override fun onStart() {
        super.onStart()
        // Register listener on a background Looper before tightening the budget
        budgetManager.registerProcessOverBudgetListener(cleanupThread.looper, overBudgetListener)
        // Constrain memory during lightweight gallery browsing
        applyGalleryBudget()
    }

    override fun onStop() {
        super.onStop()
        budgetManager.unregisterProcessOverBudgetListener(overBudgetListener)
    }

    override fun onDestroy() {
        cleanupThread.quitSafely()
        super.onDestroy()
    }

    /**
     * Called when the user enters the high-resolution editing canvas.
     * Clears the dynamic budget, restoring the full 256MB manifest ceiling.
     */
    fun enterEditingCanvas() {
        // Clear the tighter dynamic budget to restore the full manifest ceiling (256MB)
        budgetManager.clearProcessBudget()
        Log.i(TAG, "Restored manifest budget ceiling (256MB) for editing canvas")
    }

    /**
     * Called when the user exits the editor back to the thumbnail gallery.
     * Re-applies the tighter dynamic budget.
     */
    fun exitToGallery() {
        previewCache.trimToSize(8 * 1024 * 1024)
        applyGalleryBudget()
    }

    private fun applyGalleryBudget() {
        try {
            // Dynamically tighten budget to 96MB for the lightweight gallery view
            budgetManager.processBudgetBytes = 96L * 1024L * 1024L
            Log.i(TAG, "Tighter dynamic budget applied for gallery: 96MB")
        } catch (e: IllegalArgumentException) {
            Log.e(TAG, "Could not apply dynamic budget", e)
        }
    }

    companion object {
        private const val TAG = "ImageEditor"
    }
}

Exemplo de NDK C++: mecanismo 3D nativo

Este exemplo mostra um mecanismo de jogo nativo em C++ que gerencia orçamentos de memória com base no nível de qualidade gráfica ativo, supondo que o manifesto do aplicativo declare um limite de 512 MB para acomodar gráficos de alta qualidade (android:maxMb="512"). O mecanismo inicia AMemoryBudgetManager_Watcher_create em um ALooper de linha de execução de worker em segundo plano antes de reduzir o orçamento para predefinições de qualidade mais baixa e descarrega mipmaps de textura quando o orçamento é excedido.

#include <android/memory_budget_manager.h>
#include <android/looper.h>
#include <android/log.h>

#define LOG_TAG "Native3DEngineMemory"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__)

class MemoryGovernor {
public:
    MemoryGovernor() : mWatcher(nullptr) {}

    ~MemoryGovernor() {
        stopMonitoring();
    }

    // Configures process budget based on user graphics quality settings
    bool setQualityBudget(int qualityLevel) {
        int64_t targetBytes = 0;
        switch (qualityLevel) {
            case 0: // Low (budget: 128MB)
                targetBytes = 128LL * 1024 * 1024;
                break;
            case 1: // Medium (budget: 256MB)
                targetBytes = 256LL * 1024 * 1024;
                break;
            case 2: // High (budget: 512MB)
                targetBytes = 512LL * 1024 * 1024;
                break;
            default:
                // Clear dynamic override and restore manifest limit
                AMemoryBudgetManager_clearProcessBudget();
                return true;
        }

        AMemoryBudgetResult result = AMemoryBudgetManager_setProcessBudget(targetBytes);
        if (result != AMEMORY_BUDGET_RESULT_SUCCESS) {
            LOGW("Could not set quality budget: %s", AMemoryBudgetManager_resultToString(result));
            return false;
        }
        return true;
    }

    // Call on a background worker ALooper before tightening the budget with setQualityBudget
    bool startMonitoring(ALooper* backgroundLooper) {
        if (!backgroundLooper) return false;

        // Monitor process budget events, debounced to at most once every 1000ms
        mWatcher = AMemoryBudgetManager_Watcher_create(
            backgroundLooper,
            AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
            1000,
            &MemoryGovernor::onPressureEvent,
            this
        );
        return mWatcher != nullptr;
    }

    void stopMonitoring() {
        if (mWatcher) {
            AMemoryBudgetManager_Watcher_destroy(mWatcher);
            mWatcher = nullptr;
        }
    }

    void unloadUnusedTextures() {
        LOGW("Memory pressure callback triggered. Purging cached texture mipmaps...");
        // Fast, idempotent, high-yield eviction without allocating memory
    }

private:
    static void onPressureEvent(
        int32_t event_mask,
        const AMemoryBudgetEvents* events,
        void* userdata
    ) {
        auto* governor = static_cast<MemoryGovernor*>(userdata);
        governor->unloadUnusedTextures();
    }

    AMemoryBudgetManagerWatcher* mWatcher;
};