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 orçamento.

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á ficar mais lento.

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

Use AndroidManifest.xml para declarar seus orçamentos de memória de estado estável, que são os orçamentos de referência em que o app opera durante o uso normal. As declarações de manifesto não exigem código de execução, entram em vigor na inicialização do processo e alternam os orçamentos entre os estados foreground, perceptible e background. PackageManager.getMemoryBudgets expõe declarações de orçamento estáticas de <application>. Para validar os orçamentos candidatos em produção com reversão remota antes de confirmá-los no manifesto, consulte Implantar orçamentos em etapas com configuração remota.

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 elementos XML não reconhecidos. Portanto, é possível adotar <memory-budget> sem afetar a compatibilidade com versões anteriores.

Configurar sua versão

Não é necessário aumentar minSdk ou targetSdk para adotar orçamentos de memória do app. A declaração de limites por processo em <processes> requer o Plug-in do Android para Gradle 9.5.0 ou mais recente. Para compilar atributos de manifesto <memory-budget>, MemoryBudgetManager e Build.VERSION_CODES_FULL.CINNAMON_BUN_2, defina compileSdk como o nível 37 da API com minorApiLevel = 2 no arquivo build.gradle.kts ou build.gradle do módulo:

Kotlin

android {
    compileSdk {
        version = release(37) {
            minorApiLevel = 2
        }
    }
}

Groovy

android {
    compileSdk {
        version = release(37) {
            it.minorApiLevel = 2
        }
    }
}

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 de primeiro plano ou de estado omitido não é obrigatória. Se você declarar apenas cláusulas perceptible ou background, os estados de maior importância, como foreground, vão permanecer sem restrições de um orçamento de app. Omitir android:state usa como padrão foreground, que se propaga para perceptible e background, a menos que uma cláusula posterior substitua esses estados. Para detalhes sobre a ordem das cláusulas, consulte Regra de resolução: a última cláusula aplicável entra em vigor.

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 espaço 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 multiprocesso) para que as alocações sejam orçadas de forma independente e removidas quando o usuário sair.
  • Defina orçamentos de background de forma rigorosa para o trabalho headless: um RSS em segundo plano alto na telemetria de campo geralmente reflete as 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 impedir 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 Activity 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 aplicativo em primeiro plano.
  • perceptible: cargas de trabalho 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 ativos em segundo plano, 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 bloqueio de tela, serviço ou provedor de conteúdo vinculado pelo 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
IMPORTANCE_CACHED
Jobs em segundo plano, receptores, sincronização em segundo plano e processos em cache

Para detalhes sobre 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 continua a aplicar o orçamento de memória background, além do congelamento e da recuperação de processos armazenados em cache, mantendo o processo limitado, mesmo que o congelamento ou a compactação não ocorram.

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, ela 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 uso da memória residente atual do app:

adb shell dumpsys meminfo <package-name>

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 que ocorrer primeiro.

Escalonar orçamentos para telas de alta densidade

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

  • Escalonar 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 IU são escalonados 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 contagem de pixels da janela (android:additionalBytesPerDisplayPixel): adiciona bytes por pixel nos limites da janela ativa (WindowMetrics.getBounds largura × altura). Isso é adequado para apps que alocam superfícies gráficas de janela inteira, buffers de renderização ou caches de fotos de alta resolução, em que o consumo de memória é dimensionado diretamente com a contagem de pixels brutos, em vez de buckets de densidade de IU:

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

    Em uma janela de 1080p em tela cheia (1080 × 2400 &approx; 2,59 milhões de pixels), isso adiciona 41.472.000 bytes (&approx; 39,6 MB, em que 1 MB = 1.048.576 bytes) ao orçamento de referência. Em uma janela de 1440p (1440 × 3120 &approx; 4,49 milhões de pixels), ele adiciona 71.884.800 bytes (&approx; 68,6 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.

Nos relógios Wear OS, a RAM é limitada, e a interface e o conjunto de atributos 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 app ou processo, o sistema os avalia na ordem em que aparecem no manifesto e aplica a última cláusula de orçamento aplicável.

Um android:state omitido assume como padrão foreground, que também se aplica em cascata a perceptible e background, a menos que seja substituído por uma cláusula posterior para esses estados. Ordene as cláusulas de foreground (ou android:state omitido) para perceptible e background e coloque as substituições de android:feature no grupo de estado correspondente para que uma substituição de estado omitido final não oculte as cláusulas perceptible ou background anteriores.

Referência de atributos XML

Todos os atributos de megabyte (MB) usam unidades binárias (1 MB = 1.048.576 bytes) e são mapeados para a carga memory.current do cgroup do Linux, que exclui a memória compartilhada do 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 foreground O estado do processo ao qual esse orçamento se aplica: foreground, perceptible ou background. Se omitido, o padrão será foreground e será aplicado em cascata a perceptible e background, a menos que seja substituído por uma cláusula posterior.
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 da janela (WindowMetrics.getBounds 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 ativas 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 acessadas.

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 página, 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 é muito alto 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 é conveniente ao coletar amostras de arquivos cgroup de adb shell, porque a leitura do diretório cgroup de um app no shell pode exigir adb root. Um app pode ler o próprio uso do cgroup de processo em processo usando MemoryBudgetManager, mesmo em builds de produção.
  • 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: role listas densas e grades de mídia, mova-se entre os principais destinos, abra menus e caixas de diálogo e execute 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 ao 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, em vez de quando o processo estiver inativo em um estado armazenado em cache (IMPORTANCE_CACHED). Mesmo que o orçamento de background permaneça em vigor enquanto estiver armazenado em cache, um processo armazenado em cache inativo não executa o trabalho em segundo plano e não reflete o conjunto de trabalho dos jobs ou receptores em segundo plano.

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 codificar o caminho. 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 <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 backup de arquivos, 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 baixo.

Se você receber um erro de permissão ao ler os arquivos cgroup de um app em adb shell, execute adb root em um build userdebug ou em uma imagem de emulador sem o Google Play e tente de novo. Em builds de produção user em que adb root não está disponível, consulte MemoryBudgetManager.processCurrentUsageBytes no app ou use adb shell dumpsys meminfo <package-name> para 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 com o memory.current mais alto do valor de referência e reduza 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 refault e o tempo de CPU novamente e colete as estatísticas de frame. 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 em sequência. 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 eliminando 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 de retorno aumentam 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 chegam à linha de execução de interface e ao RenderThread, e os tempos de instabilidade, de renderização do frame e de retomada sem reinicialização aumentam.

Em comparação com o valor de referência sem restrições, um orçamento candidato é 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 não aumentam mais do que 1 ponto percentual
  • O tempo para a renderização do frame P95 não aumenta mais do que 3 ms
  • O tempo de retomada parcial (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 ponto de inflexão 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 joelho 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. Para confirmar o orçamento ativo no momento da execução, consulte MemoryBudgetManager.processBudgetBytes ou MemoryBudgetManager.packageBudgetBytes.

Validar orçamentos que não estão em primeiro plano com um retorno caloroso

Um orçamento perceptible ou background apertado 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 em 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 frames 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 morna, e adicione a mesma margem de 10% a 20%.

Calibrar o dimensionamento da tela

Se você usar 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 conseguir android:maxMb.

APIs de ambiente de execução

Além das cláusulas declarativas do manifesto <memory-budget>, o Android oferece uma API do SDK de ambiente de execução para as seguintes tarefas:

  • Implante e valide os orçamentos candidatos em etapas por trás da configuração remota antes de confirmá-los no AndroidManifest.xml.
  • Consultar o uso da memória do cgroup em tempo real e os orçamentos efetivos em processo.
  • Aperte um orçamento de processo ou pacote dinamicamente durante tarefas leves.
  • Detecte eventos de excesso de orçamento para reduzir os caches quando o sistema operacional recuperar memória.

Implantar orçamentos em etapas com a Configuração remota

Como um orçamento de ambiente de execução definido por MemoryBudgetManager só pode restringir limites dentro dos limites máximos declarados do manifesto <memory-budget> (e não pode aumentar um orçamento de manifesto restrito no ambiente de execução), não envie valores experimentais agressivos de <memory-budget> em AndroidManifest.xml esperando que eles sejam flexibilizados por uma flag de servidor. Em vez disso, implante novos orçamentos em duas fases:

  1. Valide com MemoryBudgetManager e configuração remota: envie o APK sem <memory-budget> em AndroidManifest.xml ou com um único limite de segurança generoso (sem cláusulas de manifesto perceptible ou background mais rígidas). Proteja os orçamentos candidatos por trás do experimento do lado do servidor ou da flag de configuração remota, aplique o orçamento do estado de destino nas transições do ciclo de vida (como em ProcessLifecycleOwner) enquanto captura IllegalArgumentException em dispositivos com limites mais baixos de sistema ou de escala de janela e chame clearProcessBudget ou clearPackageBudget quando a flag for desativada para que você possa reverter sem uma atualização do APK.
  2. Mudar os orçamentos comprovados para AndroidManifest.xml: depois que a telemetria de campo confirmar a estabilidade em todos os níveis de dispositivo, mova os orçamentos de estado estável de foreground, perceptible e background para cláusulas <memory-budget> em AndroidManifest.xml. As cláusulas de manifesto aplicam limites desde a inicialização do processo, alternam orçamentos entre estados do processo sem código do app e permanecem inspecionáveis por meio de PackageManager.getMemoryBudgets (que lê apenas declarações de manifesto <application> estáticas, não entradas <processes> ou chamadas MemoryBudgetManager dinâmicas).

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 o 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 process-scope and package-scope limits (LIMIT_IS_DISABLED if unset)
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 momento da 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 (for example, 96 MB)
try {
    budgetManager.processBudgetBytes = 96L * 1024L * 1024L
} catch (e: IllegalArgumentException) {
    // Thrown if the budget is <= 0 or exceeds a manifest 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 especificam limites separados para os estados foreground, perceptible e background, um orçamento dinâmico definido por MemoryBudgetManager aplica um único limite de ambiente de execução até que o código o atualize ou limpe. 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, como em ProcessLifecycleOwner ou callbacks de ciclo de vida da atividade, conforme 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 discretas do ciclo de vida 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) e 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, packageCurrentUsageBytes, processBudgetBytes e packageBudgetBytes leem os limites e o uso do cgroup em tempo real no processo por chamadas nativas locais, em vez de chamar o serviço do sistema. Embora essas consultas sejam rápidas, invoque-as sob demanda (como durante transições de tarefas ou dentro de callbacks OnOverBudgetListener) em vez de fazer pesquisas em loops curtos.

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:

  • Use 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 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 tempo de espera maior 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 destinos de alto rendimento: remover bitmaps grandes, renderizar buffers ou fechar arquivos mapeados em memória é muito mais eficaz do que liberar muitos objetos pequenos.

Exemplo da API Runtime

O exemplo a seguir demonstra como implementar a API de ambiente de execução usando a API do SDK do Android em Kotlin:

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 na galeria de miniaturas leves, 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)
        // Clear the activity-scoped gallery budget when leaving the activity.
        budgetManager.clearProcessBudget()
    }

    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"
    }
}