Gerenciar a memória do app

Esta página ensina a 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.

Monitorar a disponibilidade 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 a encontrar e diagnosticar problemas de memória. Confira o que ele permite fazer:

  • Conferir como o app aloca memória ao longo do tempo. O Memory Profiler mostra um gráfico em tempo real de quanta memória o app está usando, do número de objetos Java alocados e de quando ocorre a coleta de lixo.
  • Iniciar os eventos de coleta de lixo e fazer uma captura de tela do heap Java enquanto o app é executado.
  • Registrar as alocações de memória do app, inspecionar todos os objetos alocados, conferir cada alocação no stack trace e acessar o código correspondente no editor do Android Studio.

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 interface ComponentCallbacks2 nas classes Activity. O método de callback onTrimMemory() fornecido permite que o app detecte eventos relacionados à memória independente de estar em primeiro ou segundo plano. Ele também pode liberar objetos em resposta ao próprio ciclo de vida ou aos eventos que indicam que o sistema precisa de memória.

Implemente o callback onTrimMemory() para responder a diferentes eventos relacionados à memória, conforme mostrado neste exemplo:

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) {

        // Determine which lifecycle or system event is raised.
        when (level) {

            ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> {
                /*
                   Release any UI objects that currently hold memory.

                   The user interface moves to the background.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
            ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> {
                /*
                   Release any memory your app doesn't need to run.

                   The device is running low on memory while the app is running.
                   The event raised indicates the severity of the memory-related event.
                   If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system
                   begins stopping background processes.
                */
            }

            ComponentCallbacks2.TRIM_MEMORY_BACKGROUND,
            ComponentCallbacks2.TRIM_MEMORY_MODERATE,
            ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> {
                /*
                   Release as much memory as the process can.

                   The app is on the LRU list and the system is running low on memory.
                   The event raised indicates where the app sits within the LRU list.
                   If the event is TRIM_MEMORY_COMPLETE, the process is one of the
                   first to be terminated.
                */
            }

            else -> {
                /*
                  Release any non-critical data structures.

                  The app receives an unrecognized memory level value
                  from the system. Treat this as a generic low-memory message.
                */
            }
        }
    }
}

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) {

        // Determine which lifecycle or system event is raised.
        switch (level) {

            case ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN:

                /*
                   Release any UI objects that currently hold memory.

                   The user interface moves to the background.
                */

                break;

            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE:
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW:
            case ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL:

                /*
                   Release any memory your app doesn't need to run.

                   The device is running low on memory while the app is running.
                   The event raised indicates the severity of the memory-related event.
                   If the event is TRIM_MEMORY_RUNNING_CRITICAL, then the system
                   begins stopping background processes.
                */

                break;

            case ComponentCallbacks2.TRIM_MEMORY_BACKGROUND:
            case ComponentCallbacks2.TRIM_MEMORY_MODERATE:
            case ComponentCallbacks2.TRIM_MEMORY_COMPLETE:

                /*
                   Release as much memory as the process can.

                   The app is on the LRU list and the system is running low on memory.
                   The event raised indicates where the app sits within the LRU list.
                   If the event is TRIM_MEMORY_COMPLETE, the process is one of the
                   first to be terminated.
                */

                break;

            default:
                /*
                  Release any non-critical data structures.

                  The app receives an unrecognized memory level value
                  from the system. Treat this as a generic low-memory message.
                */
                break;
        }
    }
}

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 do heap atribuído a cada app. O limite exato 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 objeto ActivityManager.MemoryInfo 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 abaixo mostra como usar o método getMemoryInfo() no seu 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;
}

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 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 troca 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 o 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 bastante 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, criando 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, abstrações são significativamente mais pesadas, já que costumam exigir mais código que precisa ser executado, o que demanda mais tempo e RAM para mapear o código na memória. Evite usar abstrações se elas não oferecerem um benefício significativo.

Usar protobufs leves para dados serializados

Os buffers de protocolo (protobufs) (link em inglês) 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 pode causar muitos problemas no app, como maior uso de RAM, aumento significativo no tamanho do APK e execução mais lenta.

Para saber mais, consulte o README do protobuf.

Evitar rotatividade de memória

Os eventos de coleta de lixo não afetam a performance do app. No entanto, muitos deles que ocorrem em um curto período podem descarregar a bateria rapidamente e aumentar marginalmente o tempo de exibiçã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 então você pode criar novos objetos Paint ou Bitmap dentro da função onDraw() 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 mais 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 os problemas 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 (link em inglês).

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 uma 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).

Remover recursos e bibliotecas que consomem muita memória

Alguns recursos e bibliotecas do código podem consumir memória sem que você perceba. O tamanho total do seu APK, incluindo bibliotecas de terceiros e recursos incorporados, pode afetar a quantidade de memória que o app 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 APK

Você pode diminuir significativamente o uso da memória do seu app reduzindo o tamanho geral dele. O tamanho de bitmaps, recursos, frames de animação e bibliotecas de terceiros podem aumentar o tamanho do app. O Android Studio e o Android SDK oferecem várias ferramentas para reduzir o tamanho dos seus recursos e das dependências externas. Essas ferramentas têm suporte a métodos modernos de redução de código, como a Compilação R8.

Para saber mais sobre como reduzir o tamanho geral do app, consulte Reduzir o tamanho do app.

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

Frameworks de injeção de dependência podem simplificar o código programado e fornecer um ambiente adaptável ú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, recomendamos 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 execução ou uso de 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 um valor mais significativo de ciclos de CPU e RAM, bem como causar um atraso considerável na inicialização do app.

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.

Com as flags corretas, o ProGuard pode remover APIs e recursos, mas não pode remover grandes dependências internas de uma biblioteca. Os recursos necessários nessas bibliotecas podem exigir dependências de nível inferior. Isso se torna particularmente problemático quando você usa uma subclasse Activity de uma biblioteca, que costuma ter várias dependências, quando as bibliotecas usam reflexão, o que é comum e significa que você precisa ajustar o ProGuard manualmente para fazer a biblioteca funcionar.

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. Você também pode criar uma implementação própria.