Definir orçamentos de memória de apps

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

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

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

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

O que acontece quando um app excede o orçamento

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

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

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

Declarar orçamentos no manifesto do Android

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

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

Declarar um orçamento de referência

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

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

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

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

    </application>
</manifest>

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

Variar os orçamentos por estado do processo

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

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

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

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

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

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

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

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

    </application>
</manifest>

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

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

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

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

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

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

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

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

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

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

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

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

adb shell dumpsys activity processes <package-name>

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

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

Nesta saída:

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

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

adb shell dumpsys meminfo <package-name>

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

Apps de vários processos

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

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

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

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

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

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

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

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

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

    </application>
</manifest>

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

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

Ajustar orçamentos para telas de alta densidade

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

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

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

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

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

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

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

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

Especialização para formatos de dispositivo

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

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

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

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

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

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

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

Referência de atributos XML

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

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

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

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

A API de ambiente de execução permite:

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

API do SDK do Android (MemoryBudgetManager)

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

Recuperar o serviço

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

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

Consultar uso e orçamentos

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

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

Definir ou limpar orçamentos de forma dinâmica

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

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

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

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

Desempenho e idempotência

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

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

Detectar callbacks de pressão de orçamento excedido

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

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

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

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

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

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

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

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

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

Configuração do CMake

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

Incluir uso de cabeçalho e consulta

#include <android/memory_budget_manager.h>

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

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

Configurar dinamicamente o orçamento nativo

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

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

Monitorar eventos de pressão de memória

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

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

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

// When done:
AMemoryBudgetManager_Watcher_destroy(watcher);

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

Exemplos da API Runtime

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

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

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

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

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

package com.example.imageeditor.ui

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

class ImageEditorActivity : Activity() {

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

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

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

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

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

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

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

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

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

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

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

Exemplo de NDK C++: mecanismo 3D nativo

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

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

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

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

    ~MemoryGovernor() {
        stopMonitoring();
    }

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

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

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

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

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

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

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

    AMemoryBudgetManagerWatcher* mWatcher;
};