Definir orçamentos de memória de apps

Com os orçamentos de memória do app, os apps podem declarar 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 é especialmente útil para apps do sistema e agrupados ou para apps direcionados a dispositivos com restrição de memória, em que o desenvolvedor conhece o conjunto de trabalho de memória esperado e quer garantir que o app não use muitos recursos de RAM compartilhados do sistema.

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

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

Desde que o orçamento não exceda o conjunto de trabalho, 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 páginas de heap inativas para troca, de modo que as alocações de memória permanecem limitadas sem encerrar o processo.

Declarar orçamentos no manifesto do Android

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

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 corta as páginas de memória inativas usando remoção e troca.

Variar orçamentos por estado do processo

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

  • Em primeiro plano: o processo está hospedando uma atividade visível que interage com o usuário. Esse estado geralmente 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, hospedando um serviço em primeiro plano de reprodução de mídia, 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 deve manter um consumo mínimo.

É 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>

Não é necessário especificar android:state="foreground" na primeira cláusula. Uma cláusula sem android:state funciona como o fallback padrão para todos os estados. As cláusulas mais restritivas abaixo substituem o orçamento quando o app faz a transição para os estados perceptible ou background.

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 um 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 downloads. 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" />

        <!-- Dedicated background sync process -->
        <processes>
            <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>

O uso de memória em qualquer subproceso conta para o orçamento do processo e do pacote. Um processo sofre pressão de memória durante a execução se viola o orçamento do processo ou do pacote, o que ocorrer primeiro.

Ajustar orçamentos para telas de alta densidade

Para aplicativos cujo consumo de memória aumenta significativamente com a quantidade de pixels a serem 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:

  • Escalonar por agrupamento de densidade de exibição (android:additionalMbPerDensity): adiciona megabytes proporcionalmente à taxa de densidade da tela em relação a mdpi (1,0x / 160 dpi). Isso é adequado quando o uso de memória é dimensionado com buckets de densidade da UI, como o armazenamento em cache de elementos gráficos rasterizados de alta resolução ou recursos da UI:

    <!-- Baseline 180MB + 16MB per 1.0x density ratio -->
    <memory-budget
        android:maxMb="180"
        android:additionalMbPerDensity="16" />
    

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

  • Dimensionar pela resolução de exibição física (android:additionalBytesPerDisplayPixel): adiciona bytes diretamente por pixel de exibição 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 total em que o consumo de memória é dimensionado diretamente com a contagem de pixels brutos da tela 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 1080p (1080 × 2400 &approx; 2,59 milhões de pixels), isso adiciona &approx; 41,4 MB ao orçamento de base. Em uma tela de 1440p (1440 × 3120 ≈ 4,49 milhões de pixels), ele adiciona ≈ 71,8 MB.

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

Especializar para formatos de dispositivos

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

Em 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 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 primeiro o orçamento de referência mais geral, seguido por substituições mais específicas, como cláusulas específicas de estado ou hardware.

Referência de atributos XML

Todos os atributos de tamanho da memória são expressos em megabytes (MB) e mapeados para a carga do cgroup memory.current 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 a que este orçamento se aplica: foreground, perceptible ou background.
android:additionalMbPerDensity Número inteiro (≥ 0) 0 Megabytes adicionais a serem adicionados por unidade de proporção de densidade de exibição em relação a mdpi (1,0x).
android:additionalBytesPerDisplayPixel Número inteiro (≥ 0) 0 Bytes extras alocados por pixel de exibição física (largura × altura), útil 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 Runtime (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 tempo de execução, o Android oferece APIs de SDK e NDK de tempo de execução como uma opção secundária.

Com a API de ambiente de execução, você pode:

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

API Kotlin (MemoryBudgetManager)

O serviço do sistema MemoryBudgetManager está disponível a partir do Android 17 QPR2 (lançamento do SDK do Android 26Q4, nível da API 37.2 / Build.VERSION_CODES_FULL.CINNAMON_BUN_2).

Recuperar o serviço

val budgetManager = context.getSystemService(MemoryBudgetManager::class.java)

Uso de consultas 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 momento da execução para limitar a memória durante tarefas leves ou limpar o orçamento 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-declared ceiling
    Log.e(TAG, "Requested budget exceeds manifest or system ceiling", e)
}

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

Ouvir callbacks de pressão de orçamento excedido

Os apps podem registrar um listener para receber uma notificação quando o uso da memória exceder o limite do orçamento. Isso permite que o app realize uma limpeza proativa no nível do aplicativo (como limpar caches de bitmap na memória) antes que o sistema operacional aciona a latência de recuperação direta:

val listener = MemoryBudgetManager.OnOverBudgetListener { budgetBytes ->
    Log.w(TAG, "Process exceeded memory budget of $budgetBytes bytes")
    // Proactively evict caches to release memory
    imageTileCache.evictAll()
}

// Register on the main Looper
budgetManager.registerProcessOverBudgetListener(mainLooper, listener)

// When done (e.g., in onStop)
budgetManager.unregisterProcessOverBudgetListener(listener)

Práticas recomendadas para callbacks acima do orçamento:

  • 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.
  • Foco em metas de alto rendimento: desalojar bitmaps grandes, buffers de renderização ou fechar arquivos mapeados na 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.

Configuração do CMake

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

Incluir o uso de cabeçalhos e consultas

#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
AMemoryBudgetManager_clearProcessBudget();

Monitorar eventos de pressão da 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 remoção de duplicação automática.
  2. Descritor de arquivo de baixo nível: AMemoryBudgetManager_getProcessMemoryPressureFd retorna um descritor de 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 an ALooper with a 1000ms debounce interval
AMemoryBudgetManagerWatcher* watcher = AMemoryBudgetManager_Watcher_create(
    looper,
    AMEMORY_BUDGET_MANAGER_EVENT_PROCESS,
    1000 /* debounce_ms */,
    &onMemoryPressure,
    NULL /* userdata */
);

// When done:
AMemoryBudgetManager_Watcher_destroy(watcher);

Exemplos da API Runtime

Os exemplos a seguir mostram como implementar as APIs de tempo de execução em Kotlin e C++.

Exemplo em Kotlin: editor de imagens adaptável

Este exemplo mostra um app de edição de imagens (com.example.imageeditor) que aumenta dinamicamente o orçamento de memória quando o usuário abre uma tela de edição de várias camadas e limpa o orçamento dinâmico ao retornar para a visualização da galeria de miniaturas. Ele também registra um OnOverBudgetListener para remover bitmaps de visualização em cache sob pressão.

package com.example.imageeditor.ui

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

class ImageEditorActivity : Activity() {

    private lateinit var budgetManager: MemoryBudgetManager

    // 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)
    }

    override fun onStart() {
        super.onStart()
        // Register listener for process-level memory breaches
        budgetManager.registerProcessOverBudgetListener(mainLooper, overBudgetListener)
    }

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

    /**
     * Called when the user enters the high-resolution editing canvas.
     */
    fun enterEditingCanvas() {
        try {
            // Dynamically set budget to 256MB for the editing canvas
            budgetManager.processBudgetBytes = 256L * 1024L * 1024L
            Log.i(TAG, "Dynamic budget applied: 256MB")
        } catch (e: IllegalArgumentException) {
            Log.e(TAG, "Could not apply dynamic budget", e)
        }
    }

    /**
     * Called when the user exits the editor back to the thumbnail gallery.
     */
    fun exitToGallery() {
        previewCache.trimToSize(8 * 1024 * 1024)
        // Clear dynamic budget; restores the baseline manifest budget
        budgetManager.clearProcessBudget()
    }

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

Exemplo de NDK C++: mecanismo 3D nativo

Este exemplo mostra um mecanismo de jogo nativo em C++ gerenciando orçamentos de memória com base no nível de qualidade gráfica ativo, usando AMemoryBudgetManager_Watcher_create em um ALooper para descarregar 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;
    }

    bool startMonitoring(ALooper* looper) {
        if (!looper) return false;

        // Monitor process budget events, debounced to at most once every 1000ms
        mWatcher = AMemoryBudgetManager_Watcher_create(
            looper,
            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, 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;
};