Gerenciar a memória do app

Esta página explica como reduzir proativamente o uso da memória no seu app. Para saber mais sobre como o sistema operacional Android gerencia a memória, consulte a Visão geral do gerenciamento de memória.

A memória de acesso aleatório (RAM) é um recurso valioso em qualquer ambiente de desenvolvimento de software, sendo ainda mais importante em um sistema operacional móvel em que a memória física costuma ser restrita. Embora o Android Runtime (ART) e a máquina virtual Dalvik façam a coleta de lixo de rotina, isso não significa que você pode ignorar o momento e o local em que seu app aloca e libera memória. Ainda é necessário evitar introduzir vazamentos de memória, que geralmente ocorrem quando referências de objetos são armazenadas em variáveis de membros estáticas, e liberar objetos Reference no momento adequado, conforme definido pelos callbacks do ciclo de vida.

Reduzir o código e o uso de recursos do app

Alguns recursos e bibliotecas do código podem consumir memória sem que você perceba. O tamanho total do seu app, incluindo bibliotecas de terceiros ou recursos incorporados, pode afetar a quantidade de memória que ele consome. Você pode melhorar o consumo de memória do seu app removendo do código os componentes, bibliotecas e recursos redundantes, desnecessários ou pesados.

Reduzir o tamanho geral do app ativando o R8

O código do aplicativo compilado é uma parte ativa do uso da memória de execução. Cada classe, método, dependência de biblioteca e constante de string precisa ser carregado na RAM quando executado. Quanto maior for a base de código compilada, mais RAM física o app vai precisar.

Você pode usar o R8 para reduzir o consumo de memória do app. Embora o R8 seja tradicionalmente conhecido por reduzir o tamanho do APK, ele tem um impacto positivo direto na memória de execução (RAM). O R8 analisa o bytecode do app para remover códigos inativos, mesclar classes redundantes, métodos inline e reduzir identificadores. Ao carregar menos bytecode compilado do APK na RAM, ele diminui o consumo de memória de referência geral do app. Além disso, a redução dos nomes de classes, métodos e campos em identificadores mais curtos reduz diretamente a sobrecarga da RAM. Otimizações como a mesclagem de classes e a inclusão de métodos extensivos também substituem pesquisas e padrões de alocação de tempo de execução caros, resultando em memória de heap e pilha otimizada.

Entender as regras keep

As regras keep são instruções de configuração que informam ao R8 quais partes do código preservar durante a otimização, impedindo que ele remova ou reduza o código de que o app depende. Para mais informações, consulte a Visão geral das regras keep.

Regras keep mal escritas impedem que o R8 otimize grandes partes da base de código. Evite regras keep muito amplas e siga estas práticas recomendadas:

  • Regras globais a serem evitadas:
    • -dontoptimize: desativa completamente a otimização para todo o app, resultando em executáveis maiores e mais lentos.
    • -dontshrink: impede a remoção de códigos e recursos não utilizados.
    • -dontobfuscate: impede a redução de nomes, perdendo economias de memória valiosas (especialmente em apps grandes).
  • Evite caracteres curinga em todo o pacote:regras amplas como -keep class com.example.package.** { *; } forçam o R8 a preservar todas as classes, campos e métodos nesse pacote. Isso interrompe completamente a capacidade do R8 de remover, otimizar ou reduzir o código nesse pacote.

  • Use o arquivo de configuração padrão do R8:sempre use proguard-android-optimize.txt.

Para mais informações sobre como escrever regras keep, consulte a Visão geral das regras keep. Para padrões específicos a serem usados e evitados, consulte Práticas recomendadas de regras keep.

O R8 Configuration Analyzer fornece insights sobre a configuração do R8 e como cada regra keep afeta o app. Para mais informações sobre como identificar regras que bloqueiam a otimização, consulte R8 Configuration Analyzer.

Cuidado ao usar bibliotecas externas

Códigos de bibliotecas externas geralmente não são projetados para ambientes de dispositivos móveis e podem ser ineficientes para um cliente móvel. Esse tipo de biblioteca pode precisar ser otimizado para dispositivos móveis. Planeje esse trabalho com antecedência e analise a biblioteca em termos de tamanho de código e uso de recursos de RAM antes de usá-la.

Até mesmo algumas bibliotecas otimizadas para dispositivos móveis podem causar problemas devido a implementações diferentes. Por exemplo, uma biblioteca pode usar protobufs leves enquanto outra usa microprotobufs, resultando em duas implementações de buffers de protocolo diferentes no app. Isso pode acontecer com várias implementações de geração de registros, análise, frameworks de carregamento de imagens, armazenamento em cache e muitos outros fatores inesperados.

Embora a otimização do app usando o R8 possa remover códigos não utilizados de dependências, a eficácia dele geralmente é limitada pela configuração interna da biblioteca. Por exemplo, regras keep amplas ou o uso de reflexão em uma biblioteca podem impedir que o R8 reduza o código, levando a um uso de memória maior. Para estratégias sobre como selecionar bibliotecas eficientes, consulte Escolher bibliotecas com sabedoria.

Evite usar uma biblioteca compartilhada para apenas um ou dois recursos de dezenas. Não importe uma grande quantidade de código e sobrecarga que você não vai usar. Quando você quiser usar uma biblioteca, procure uma implementação que corresponda às suas necessidades. Caso contrário, você pode criar uma implementação própria.

Usar Hilt ou Dagger 2 para injeção de dependência

Frameworks de injeção de dependência podem simplificar o código que você cria e fornecer um ambiente adaptável. Isso é útil para testes e outras mudanças na configuração.

Se você pretende usar um framework de injeção de dependência no seu app, considere usar Hilt ou Dagger. A Hilt é uma biblioteca de injeção de dependência para Android executada com base no framework Dagger. O Dagger não usa reflexão para verificar o código do app. Use a implementação de compilação estática da Dagger em apps Android sem custo desnecessário de tempo de execução ou uso da memória.

Outros frameworks de injeção de dependência que usam reflexão inicializam processos verificando o código em busca de anotações. Esse processo pode exigir significativamente mais ciclos de CPU e RAM, além de causar um atraso considerável quando o app é iniciado.

Ao usar a injeção de dependência, tenha cuidado para evitar vazamentos de memória, garantindo que os objetos tenham o escopo adequado. Manter objetos por mais tempo do que o necessário, vinculando-os ao ciclo de vida errado, pode levar a vazamentos de memória.

Ser intencional com o carregamento de imagens

Os bitmaps gráficos geralmente são os maiores objetos comuns que residem na memória do app. Mesmo que você esteja trabalhando com arquivos compactados, como JPEGs, o arquivo precisa ser inflado em um bitmap não compactado para ser mostrado na tela. Um pequeno arquivo de imagem compactado pode se expandir em um bitmap muito grande.

Por exemplo, a maioria dos bitmaps usa a configuração ARGB_8888, o que significa que cada pixel requer 4 bytes de memória: um byte para vermelho, verde, azul e alfa (transparência). Se você tiver um JPEG de 100 KB e o mostrar em uma visualização de 1000 x 1000 pixels, o bitmap vai exigir 4 bytes para cada um desses 1.000.000 pixels, totalizando 4 MB de memória.

Há várias medidas que você pode tomar para otimizar o uso de imagens. Por exemplo, o uso de bibliotecas de carregamento de imagens pode ajudar a liberar memória quando ela não for necessária. Para informações sobre como processar imagens de maneira eficiente, consulte Otimizar imagens de bitmap.

Monitorar a memória disponível e o uso da memória

Para corrigir os problemas de uso da memória do seu app, você precisa encontrá-los. O Memory Profiler do Android Studio ajuda você a encontrar e diagnosticar problemas de memória porque permite:

O Memory Profiler também é integrado à biblioteca de detecção de vazamentos LeakCanary. Ao usar o LeakCanary, você pode mover a análise de vazamento de memória do dispositivo de teste para a máquina de desenvolvimento, o que pode acelerar significativamente o fluxo de trabalho. Para mais informações, consulte as notas da versão do Android Studio.

Há outras ferramentas que você pode usar para diagnosticar problemas de memória com base em dados de usuários que executam o app de produção:

Liberar memória em resposta a eventos

O Android pode liberar a memória do app ou encerrá-lo totalmente, se necessário para reduzir o uso de memória e permitir a execução de tarefas essenciais, conforme explicado na Visão geral do gerenciamento de memória. Para equilibrar ainda mais a memória do sistema e evitar a necessidade de encerrar o processo do app, implemente a ComponentCallbacks2 interface nas classes Activity. O método de callback onTrimMemory() fornecido notifica o app sobre eventos relacionados ao ciclo de vida ou à memória que apresentam uma boa oportunidade para o app reduzir voluntariamente o uso da memória. A liberação de memória pode reduzir a frequência com que o app é encerrado pelo low-memory killer.

A implementação de onTrimMemory() precisa se concentrar exclusivamente nos eventos TRIM_MEMORY_UI_HIDDEN e TRIM_MEMORY_BACKGROUND. A partir do Android 14, o sistema não entrega mais notificações para as outras constantes legadas. Essas constantes foram descontinuadas formalmente no Android 15.

  • TRIM_MEMORY_UI_HIDDEN: esse sinal indica que a interface do app saiu da visualização do usuário. Essa transição oferece uma oportunidade de liberar alocações de memória substanciais estritamente vinculadas à interface, como bitmaps, buffers de reprodução de vídeo ou recursos de animação complexos.

  • TRIM_MEMORY_BACKGROUND: esse sinal indica que o processo está em segundo plano e agora é um candidato ao encerramento para atender às necessidades globais de memória do sistema. Para estender a duração do processo no estado armazenado em cache e reduzir o número de inicializações a frio do app, libere agressivamente todos os recursos que podem ser facilmente reconstruídos quando o usuário retomar a sessão.

Este exemplo de código mostra como implementar o callback onTrimMemory() para responder a diferentes eventos relacionados à memória:

Kotlin

import android.content.ComponentCallbacks2
// Other import statements.

class MainActivity : AppCompatActivity(), ComponentCallbacks2 {

    // Other activity code.

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that is raised.
     */
    override fun onTrimMemory(level: Int) {

        if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
            // Release memory related to UI elements, such as bitmap caches.
        }

        if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
            // Release memory related to background processing, such as by
            // closing a database connection.
        }
    }
}

Java

import android.content.ComponentCallbacks2;
// Other import statements.

public class MainActivity extends AppCompatActivity
    implements ComponentCallbacks2 {

    // Other activity code.

    /**
     * Release memory when the UI becomes hidden or when system resources become low.
     * @param level the memory-related event that is raised.
     */
    public void onTrimMemory(int level) {

        if (level >= ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN) {
            // Release memory related to UI elements, such as bitmap caches.
        }

        if (level >= ComponentCallbacks2.TRIM_MEMORY_BACKGROUND) {
            // Release memory related to background processing, such as by
            // closing a database connection.
        }
    }
}

Conferir a quantidade de memória necessária

Para permitir vários processos em execução, o Android define um limite rígido para o tamanho de heap atribuído a cada app. O limite exato para o tamanho de heap varia entre dispositivos com base na quantidade de RAM disponível. Se o app atingir a capacidade de heap e tentar alocar mais memória, o sistema vai gerar um OutOfMemoryError.

Para evitar a falta de memória, consulte o sistema e determine quanto espaço de heap há disponível no dispositivo atual. Para fazer isso, chame getMemoryInfo(). O sistema retornará um ActivityManager.MemoryInfo objeto com informações sobre o status atual da memória do dispositivo, incluindo a quantidade disponível, o total e o limite, ou seja, o nível de memória em que o sistema começa a encerrar processos. O objeto ActivityManager.MemoryInfo também expõe lowMemory, um booleano simples que informa se a memória do dispositivo está acabando.

O snippet de código de exemplo a seguir mostra como usar o método getMemoryInfo() no app.

Kotlin

fun doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check whether the device is in a low memory state.
    if (!getAvailableMemory().lowMemory) {
        // Do memory intensive work.
    }
}

// Get a MemoryInfo object for the device's current memory status.
private fun getAvailableMemory(): ActivityManager.MemoryInfo {
    val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
    return ActivityManager.MemoryInfo().also { memoryInfo ->
        activityManager.getMemoryInfo(memoryInfo)
    }
}

Java

public void doSomethingMemoryIntensive() {

    // Before doing something that requires a lot of memory,
    // check whether the device is in a low memory state.
    ActivityManager.MemoryInfo memoryInfo = getAvailableMemory();

    if (!memoryInfo.lowMemory) {
        // Do memory intensive work.
    }
}

// Get a MemoryInfo object for the device's current memory status.
private ActivityManager.MemoryInfo getAvailableMemory() {
    ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
    ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
    activityManager.getMemoryInfo(memoryInfo);
    return memoryInfo;
}

Monitorar falhas por pouca memória

As falhas por pouca memória (LMKs, na sigla em inglês) visíveis ao usuário ocorrem quando a memória do sistema fica criticamente baixa. Quando a memória está baixa, o lmkd (daemon low memory killer ) encerra processos com base na oom_adj_score. Os apps armazenados em cache ou que executam um serviço sem interface associada (como um job) têm as pontuações mais altas e são encerrados primeiro. Se a memória continuar criticamente baixa, o daemon será forçado a recuperar a memória de processos com uma oom_adj_score de 0. Como essa pontuação é reservada para apps visíveis, o encerramento deles resulta em uma saída de processo imediata e não normal. Para o usuário final, parece que o app falhou, muitas vezes ignorando os mecanismos padrão de salvamento de estado do ciclo de vida e resultando na perda do progresso do usuário.

As falhas de processos em primeiro plano são um foco principal no Android Vitals porque servem como um proxy de alta fidelidade para o gerenciamento inadequado de memória. Embora qualquer taxa de LMK maior que 1% indique uma necessidade crítica de ação imediata, uma taxa baixa não é necessariamente um indicador de integridade. Uma taxa de LMK baixa percebida pelo usuário pode significar que o daemon LMK está encerrando processos com frequência enquanto eles estão em segundo plano, o que degrada a performance de "inicialização com estado salvo" e a fluidez multitarefas. Portanto, recomendamos seguir as práticas recomendadas de memória, independentemente da pontuação atual de LMK, para garantir a estabilidade de longo prazo e a integridade do dispositivo.

Usar ProfilingManager para rastrear problemas de memória

A plataforma Android fornece ProfilingManager, uma API de observabilidade avançada que permite capturar dados do usuário na produção com base nos acionadores definidos. Isso pode ajudar a identificar problemas de memória difíceis de reproduzir.

Dois novos acionadores introduzidos no Android 17 são especialmente úteis para detectar problemas de memória:

  • TRIGGER_TYPE_OOM indica que o app gerou um OutOfMemoryError. Ele é acionado na próxima vez que o app é iniciado após a falha, quando o app se registra para acionadores de criação de perfil.
  • TRIGGER_TYPE_ANOMALY é acionado quando o sistema detecta um comportamento anômalo do app. Entre outras coisas, isso pode ser acionado pelo uso da memória excessivo. Ele é acionado depois que o app apresenta uso da memória excessivo e antes que o sistema tome qualquer ação para interromper o processo ofensivo. Por exemplo, se o app exceder os limites de memória introduzidos no Android 17, TRIGGER_TYPE_ANOMALY será acionado antes que o sistema encerre o app.

Para mais informações sobre como usar ProfilingManager para registrar e recuperar acionadores de maneira programática, consulte a documentação de criação de perfil baseada em acionadores.

Você também pode usar a criação de perfil orientada por apps para definir manualmente os pontos de início e fim do rastreamento. Recomendamos fazer isso para capturar manualmente dumps de heap ou perfis de heap em áreas que você suspeita que possam ter vazamentos de memória ou uso excessivo de memória.

Usar mais construções de código com eficiência de memória

Alguns recursos do Android, classes Java e construções de código usam mais memória que outros. É possível minimizar a quantidade de memória que o app usa escolhendo alternativas mais eficientes no seu código.

Usar os serviços com moderação

É altamente recomendável não deixar os serviços em execução sem necessidade. Esse é um dos piores erros de gerenciamento de memória que um app Android pode cometer. Se o app precisa que um serviço atue em segundo plano, não o deixe em execução, a menos que ele precise realizar um job. Encerre o serviço quando ele concluir a tarefa. Caso contrário, você pode causar um vazamento de memória.

Quando você inicia um serviço, o sistema prefere manter o processo dele em execução. Esse comportamento aumenta muito o peso dos processos de serviços, já que a RAM usada por um serviço permanece indisponível para outros processos. Isso reduz o número de processos que o sistema pode manter no cache de LRU, tornando a alternância de apps menos eficiente. Isso pode até gerar uma sobrecarga no sistema quando a memória estiver baixa, e o sistema não conseguirá manter processos suficientes para hospedar todos os serviços em execução.

Evite usar serviços persistentes, que criam demandas contínuas para a memória disponível. Em vez disso, recomendamos que você use uma implementação alternativa, como WorkManager. Para saber mais sobre como usar WorkManager para programar processos em segundo plano, consulte Trabalho persistente.

Usar contêineres de dados otimizados

Algumas das classes fornecidas pela linguagem de programação não estão otimizadas para uso em dispositivos móveis. Por exemplo, a implementação genérica HashMap pode ser ineficiente para a memória , já que precisa de um objeto de entrada diferente para cada mapeamento.

O framework do Android inclui vários contêineres de dados otimizados, como SparseArray, SparseBooleanArray e LongSparseArray. Por exemplo, as classes SparseArray são mais eficientes porque evitam a necessidade de encaixotar automaticamente a chave e, às vezes, o valor, o que cria mais um ou dois objetos por entrada.

Se necessário, use matrizes brutas para ter uma estrutura de dados enxuta.

Cuidado com abstrações de código

Geralmente, desenvolvedores usam abstrações como uma boa prática de programação porque elas podem melhorar a flexibilidade e manutenção do código. No entanto, as abstrações geralmente exigem mais código para serem executadas. Conforme detalhado em Reduzir o código e o uso de recursos do app, uma base de código compilada maior aumenta diretamente a RAM física necessária para o app. Evite usar abstrações se elas não oferecerem um benefício significativo.

Usar protobufs leves para dados serializados

Os buffers de protocolo (protobufs) são um mecanismo extensível neutro em relação à linguagem e à plataforma que foi projetado pelo Google para serializar dados estruturados de forma semelhante ao XML, mas menor, mais rápido e mais simples. Se você usar protobufs para seus dados, use sempre protobufs leves no código do lado do cliente. Protobufs normais geram códigos extremamente detalhados, o que aumenta o uso de memória do código do app (consulte Gerenciar e otimizar o uso de memória do código do app) e contribui para o aumento do tamanho do APK.

Para saber mais, consulte o README do protobuf.

Ter cuidado com vazamentos de memória

O gerenciamento inadequado de referências pode levar a vazamentos de memória em que os objetos sobrevivem aos períodos úteis, impedindo que o coletor de lixo recupere a memória do objeto vazado. Para evitar vazamentos de memória, implemente um design com reconhecimento do ciclo de vida.

Evitar rotatividade de memória

Os eventos de coleta de lixo não afetam a performance do app. No entanto, muitas ocorrências de coleta de lixo que ocorrem em um curto período podem descarregar a bateria rapidamente e aumentar marginalmente o tempo de configuração dos frames devido a interações necessárias entre o coletor de lixo e as linhas de execução do aplicativo. Quanto mais tempo o sistema passar na coleta de lixo, maior será o consumo da bateria.

Geralmente, a rotatividade de memória pode causar um grande número de eventos de coleta de lixo. Na prática, a rotatividade de memória descreve o número de objetos temporários alocados que ocorrem em determinado período.

Por exemplo, você pode alocar vários objetos temporários dentro de uma repetição de for. Ou, você pode criar novos Paint ou Bitmap objetos dentro da onDraw() função de uma visualização. Em ambos os casos, o app cria muitos objetos de forma rápida e em volume elevado. Eles podem consumir rapidamente toda a memória disponível na geração jovem, forçando uma coleta de lixo.

Use o Memory Profiler para encontrar as áreas do código em que a rotatividade de memória é alta e fazer as correções necessárias.

Depois de identificar as áreas problemáticas no seu código, reduza o número de alocações nas áreas com performance crítica. Mova itens para fora das repetições internas ou para uma estrutura de alocação baseada em fábrica.

Também é possível avaliar se pools de objetos seriam benéficos para seu caso de uso. Assim, em vez de jogar uma instância de objeto fora, você poderá liberá-la em um pool quando não for mais necessária. Na próxima vez que uma instância de objeto desse tipo for necessária, basta fazer o resgate desse pool em vez de alocar uma nova.

Avalie a performance para determinar se um pool de objetos é adequado em uma determinada situação. Há casos em que os pools de objetos podem piorar a performance. Embora evitem alocações, eles introduzem outras sobrecargas. Por exemplo, a manutenção do pool geralmente envolve a sincronização, que tem uma sobrecarga significativa. Além disso, limpar a instância do objeto em um pool para evitar vazamentos de memória durante a liberação e inicializar esse objeto durante a transferência pode gerar uma sobrecarga maior que zero.

Armazenar mais instâncias de objetos do que necessário no pool também sobrecarrega o coletor de lixo. Embora os pools de objetos reduzam o número de invocações de coleta de lixo, eles aumentam a quantidade de trabalho que precisa ser feita em cada invocação, já que ela é proporcional ao número de bytes ativos (alcançáveis).