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:
- 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.
- As páginas sujas com suporte de arquivo são gravadas de volta no armazenamento e removidas.
- 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:
- Recuperação e troca de memória: explica como o SO cobra do app o heap anônimo particular e as páginas com suporte de arquivos e recupera páginas frias quando o orçamento é excedido.
- Conceitos fundamentais: explica por que a memória Zygote compartilhada é excluída do orçamento do app e como os orçamentos residentes se relacionam com o RSS anônimo + troca na telemetria de campo.
- Bitmaps e memória: explica como os bitmaps de hardware com suporte de GPU e as alocações de DMA-BUF são contabilizados fora do heap e do orçamento de memória do app.
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
foregroundpara 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
backgroundde 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, comoBIND_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 comoPROCESS_STATE_IMPORTANT_FOREGROUND, essa constante interna não significa uma janela visível e é regida pelo orçamento deperceptible.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_FOREGROUNDIMPORTANCE_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_SERVICEIMPORTANCE_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_PERCEPTIBLEIMPORTANCE_CANT_SAVE_STATEIMPORTANCE_SERVICEIMPORTANCE_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=4estate: cur=FGSrefletem 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 comoTOP. Se estiver executando um componente importante em primeiro plano (como um download ou uma sincronização), ele vai aparecer comoIMPF.- Na tabela de controle de OOM do processo,
prcpindica que o processo é avaliado no nível de prioridade perceptível, que corresponde ao orçamento deperceptible.
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):
- Processo principal: hospeda a interface visível e o mecanismo de reprodução de áudio
(
MediaSessionServicecom um serviço em primeiro planomediaPlayback). 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 estadoperceptible, em que um orçamento de 64 MB é suficiente para o mecanismo de reprodução e o buffer de áudio. - 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 explicitamentestate="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 amdpi(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 telaxxhdpi(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.getBoundslargura × 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 ≈ 2,59 milhões de pixels), isso adiciona 41.472.000 bytes (≈ 39,6 MB, em que 1 MB = 1.048.576 bytes) ao orçamento de referência. Em uma janela de 1440p (1440 × 3120 ≈ 4,49 milhões de pixels), ele adiciona 71.884.800 bytes (≈ 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
userdebugou uma imagem de emulador sem o Google Play é conveniente ao coletar amostras de arquivos cgroup deadb shell, porque a leitura do diretório cgroup de um app no shell pode exigiradb root. Um app pode ler o próprio uso do cgroup de processo em processo usandoMemoryBudgetManager, 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 jobWorkManager(por exemplo, comadb 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.anonefileemmemory.stat: memória anônima residente e com backup de arquivos, em bytes.workingset_refault_anoneworkingset_refault_fileemmemory.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> 120MBadb 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:
- Inicie um novo processo, direcione-o para o estado de destino e aplique o orçamento.
- 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.
Redefina as estatísticas de frame e registre os contadores de refault iniciais e o tempo de CPU do processo (
utimeestime, em tiques de relógio):adb shell dumpsys gfxinfo <package-name> resetadb shell "grep -E '^workingset_refault_(anon|file) ' $CG/memory.stat"adb shell "cut -d ' ' -f 14,15 /proc/$PID/stat"Execute a passagem medida do fluxo do usuário.
Registre
memory.current,anon,file, os contadores de refault e o tempo de CPU novamente e colete as estatísticas de frame. Odumpsys gfxinfopode 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.currentsegue o orçamento eliminando páginas frias defilee heap inativo.anonpermanece 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 (
TotalTimedeadb shell am start -W) aumenta em no máximo 15% para os orçamentosperceptibleebackground
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:
- 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. - Deixe o trabalho perceptível ou em segundo plano ser executado por 10 a 15 segundos.
- Limpe a substituição (
adb shell am memory-limiter manual <PID> none) e, em seguida, retorne imediatamente o app comadb shell am start -We registreTotalTime. Se você definir o candidato usandoMemoryBudgetManager, chameclearProcessBudgetantes que o app retorne. - 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
(≈ 2,59 milhões de pixels) e 1440 × 3120 (≈ 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:
- Valide com
MemoryBudgetManagere configuração remota: envie o APK sem<memory-budget>emAndroidManifest.xmlou com um único limite de segurança generoso (sem cláusulas de manifestoperceptibleoubackgroundmais 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 emProcessLifecycleOwner) enquanto capturaIllegalArgumentExceptionem dispositivos com limites mais baixos de sistema ou de escala de janela e chameclearProcessBudgetouclearPackageBudgetquando a flag for desativada para que você possa reverter sem uma atualização do APK. - 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 deforeground,perceptibleebackgroundpara cláusulas<memory-budget>emAndroidManifest.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 dePackageManager.getMemoryBudgets(que lê apenas declarações de manifesto<application>estáticas, não entradas<processes>ou chamadasMemoryBudgetManagerdinâ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()ouclearPackageBudget()repetidamente ou quando nenhum orçamento dinâmico está ativo é uma operação segura. Da mesma forma, definirsetProcessBudgetBytes()ousetPackageBudgetBytes()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,processBudgetBytesepackageBudgetBytesleem 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 callbacksOnOverBudgetListener) 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 fundoHandlerThreadLooperpararegisterProcessOverBudgetListenerouregisterPackageOverBudgetListenersempre 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. ReserveLooper.getMainLooperpara 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(ouunregisterPackageOverBudgetListener) dentro do callback ou proteja a limpeza com um carimbo de data/hora de espera ouAtomicBoolean. - Registre-se antes de reduzir um orçamento: se o uso atual da memória já
exceder um novo orçamento quando você chamar
setProcessBudgetBytesousetPackageBudgetBytes, 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"
}
}