Priorizar a eficiência de memória: etapas essenciais para o Android 17
Leitura de 10 minutos
Embora o desempenho do app seja frequentemente associado a uma interface do usuário fluida e tempos de inicialização rápidos, a memória serve como a base silenciosa em que essas métricas visíveis são criadas. Não é segredo que estamos passando por uma mudança em que a memória do dispositivo é mais importante do que nunca. Não apenas fizemos avanços nas otimizações de memória do Android com o Android 17, mas também estamos oferecendo suporte a ferramentas e APIs para ajudar você a ficar à frente de requisitos de memória mais rigorosos ainda este ano.
Para garantir a estabilidade do dispositivo, a partir do Android 17, o sistema vai aplicar limites de memória para apps com base na RAM total do dispositivo. Se um app exceder esses limites, o Android vai encerrar o processo sem um stack trace associado.
Além dessas terminações forçadas, o uso da memória não otimizado inevitavelmente degrada a experiência do usuário. Quando o app se aproxima dos limites de memória do heap, ele aciona a coleta de lixo com frequência, o que causa travamentos perceptíveis na interface. Além disso, quando um dispositivo fica sem memória disponível, o sistema tenta recuperar páginas, causando sobrecarga na CPU, latência na interface e consumo elevado da bateria. Se a falta de memória for muito grave, ela poderá causar eventos de encerramento por pouca memória (LMK, na sigla em inglês) que encerram abruptamente os processos em segundo plano e forçam os apps a ter inicializações a frio lentas e perder o estado do usuário.
Para criar apps de alta performance e evitar essas finalizações forçadas, recomendamos que você adote as seguintes estratégias de otimização de memória:
- Maximizar a otimização de bytecode com o R8
- Otimizar o carregamento de imagens
- Detectar e corrigir vazamentos de memória com o Android Studio
- Reduzir a memória quando o app sai do estado visível
- Observabilidade avançada de memória com o ProfilingManager
Uma versão resumida desta postagem do blog também está disponível em formato de vídeo. Confira!
Noções básicas sobre os limites de memória para apps do Android 17
Os limites de memória para apps estão sendo introduzidos no Android 17 para evitar que "um usuário de má-fé" destrua a experiência multitarefa e a estabilidade de todo o dispositivo do usuário.
Confira um resumo dos motivos que impulsionam essa mudança na arquitetura:
- Prevenção de encerramentos em cascata: quando um app fica inchado ou vaza memória enquanto está em um estado privilegiado (por exemplo, executando um serviço em primeiro plano), ele é inicialmente protegido do Low Memory Killer (LMK) do sistema. À medida que esse único app cresce sem controle e acumula RAM, o LMK é forçado a compensar encerrando dezenas de apps menores e jobs em segundo plano armazenados em cache para recuperar espaço para o uso excessivo de memória.
- Preservar a multitarefa e o estado do usuário:quando o sistema é forçado a limpar apps em cache para acomodar um único processo com vazamento, a experiência multitarefa é muito prejudicada. Os usuários que retornam a aplicativos armazenados em cache anteriores encontram inicializações a frio lentas em vez de retomadas a quente quase instantâneas. Essa ineficiência gera mais carga na CPU e acelera o esgotamento da bateria. Ele também pode destruir o contexto do usuário em apps usados recentemente, como posições de rolagem, pilhas de navegação e progresso no jogo.
Para determinar se a sessão do app foi afetada por essas restrições no campo, chame getDescription() em ApplicationExitInfo. Se o sistema tiver aplicado um limite, o motivo da saída será informado como REASON_OTHER, e a string de descrição vai conter "MemoryLimiter:AnonSwap". Também é possível usar o criação de perfil com base em gatilho usando TRIGGER_TYPE_ANOMALY para capturar automaticamente despejos de heap quando o limite de memória for atingido. Além disso, o Android está trabalhando ativamente para mostrar mais métricas de memória em campo aos desenvolvedores no Google Play Console.
Também ampliamos nossa documentação sobre limites de memória para incluir comandos de depuração local. Assim, é possível simular restrições de memória no ambiente local e validar o comportamento do aplicativo em qualquer restrição de limite de memória.
Maximizar a otimização de bytecode com o R8
Uma maneira altamente eficaz de reduzir o consumo de memória do app é ativar o otimizador R8. Ao reduzir classes, métodos e campos para nomes mais curtos e remover código e recursos não utilizados, o R8 reduz significativamente o consumo de memória do app, minimizando a quantidade de código residente necessária durante a execução.
O R8 minimiza o código residente, reduzindo o consumo de memória e diminuindo o risco de encerramento do LMK. Isso resulta em inicializações com estado salvo mais frequentes do que inicializações a frio lentas. Além disso, o bytecode simplificado reduz o overhead da CPU da linha de execução principal, diminuindo diretamente as taxas de ANR para uma experiência do usuário mais fluida. Por exemplo, o banco digital Monzo ativou a otimização completa do R8 e teve uma redução de 35% na taxa de ANR, uma melhoria de 30% na taxa de inicialização a frio e uma redução de 9% no tamanho geral do app.
Para configurar corretamente o R8 no arquivo build.gradle:
- Defina
isShrinkResources = trueeisMinifyEnabled = true. - Use
proguard-android-optimize.txtem vez doproguard-android.txtlegado, que impede otimizações e não é mais compatível com o plug-in do Android para Gradle 9. - Remova
android.enableR8.fullMode = falsedo seugradle.properties.
Se você estiver usando reflexão na sua base de código, adicione regras de manutenção para evitar que o R8 otimize essas partes do código. Defina o escopo das regras de retenção de forma restrita para maximizar a otimização.
Para conseguir a otimização máxima, siga estas práticas recomendadas no arquivo de regra de permanência.
- Remova opções globais como
-dontoptimize,-dontshrinke-dontobfuscateque impedem o R8 de otimizar toda a base de código. - Remova as regras de preservação que impedem a otimização de componentes do Android, como atividades, serviços, visualizações ou broadcast receivers.
- Refine as regras de manutenção amplas em todo o pacote para segmentar apenas classes ou métodos específicos.
Para mais práticas recomendadas, consulte nossa documentação sobre regras de retenção.
Práticas recomendadas para desenvolvedores de bibliotecas R8
Se você é um desenvolvedor de biblioteca, coloque estritamente as regras que seus consumidores precisam no consumer-rules file e mantenha as regras de proteção internas da biblioteca no arquivo proguard-rules.pro. Para mais informações sobre como otimizar bibliotecas, consulte Otimização para autores de bibliotecas.
Analisador de configuração do R8
Para auditar a otimização do R8, use o Configuration Analyzer. O analisador de configuração mostra o estado atual da otimização com as pontuações de ofuscação, otimização e redução. Com o analisador de configuração, também é possível entender quantas classes, métodos ou campos são impedidos de serem otimizados por cada regra de manutenção. Refine essas regras amplas de manutenção em todo o pacote para desbloquear a otimização máxima.
Com o analisador de configuração, também é possível identificar regras de retenção que estão substituindo outras, regras redundantes e regras não utilizadas.
Habilidade do agente R8
Você também pode usar a habilidade do agente R8 com o agente do Android Studio ou outras ferramentas de IA para resolver configurações incorretas e refinar suas regras, melhorando o desempenho do app. (Os insights de habilidades baseadas em IA exigem verificação técnica)
Otimizar o carregamento de imagens
Os bitmaps geralmente são os maiores objetos comuns na memória do app. Eles representam a etapa final do processo de carregamento de imagens, em que arquivos compactados, como JPEGs ou PNGs, são decodificados em dados de pixels brutos para exibição. Isso significa que uma pequena imagem compactada de 100 KB pode ocupar vários megabytes de RAM, porque o consumo de memória é determinado pelas dimensões de pixel e pela profundidade de cor da imagem. Como as operações de bitmap geralmente estão no caminho crítico para desenhar frames, imagens não otimizadas causam um aumento significativo no uso da memória e instabilidade na interface.
O Google recomenda usar bibliotecas de carregamento de imagens Coil (link em inglês) para projetos Kotlin-first, principalmente ao desenvolver com o Jetpack Compose, e Glide (link em inglês) para aplicativos baseados em Java.
Adote estas cinco práticas recomendadas
- Reduza a resolução das imagens:se você estiver carregando bitmaps manualmente, evite carregar uma imagem grande em uma miniatura pequena. Use inSampleSize para carregar uma versão menor. O Glide e o Coil reduzem a resolução das imagens por padrão, e é possível configurar essa estratégia usando DownsampleStrategy e ImageLoader, respectivamente.
- Corte : evite incorporar padding diretamente em um arquivo de imagem para fins de letterboxing (por exemplo, criar uma borda transparente para expandir as dimensões de uma imagem). Em vez de incorporar essas bordas, use InsetDrawable ou aplique padding diretamente na View ou no elemento combinável que contém o bitmap.
- Configuração:equilibre memória e qualidade escolhendo o formato de pixel certo. Use
RGB_565quando a transparência não for necessária. Ele usa metade da memória do formatoARGB_8888padrão. No Glide, é possível configurar isso usando DecodeFormat. No Coil, use a propriedade bitmapConfig. - Priorize drawables vetoriais:para recursos geométricos básicos, use ShapeDrawable como uma alternativa leve à decodificação de bitmaps rasterizados. Ao definir esses recursos uma vez via XML, você garante que eles sejam dimensionados sem problemas em todas as densidades de tela, eliminando o aumento do uso da memória causado por recursos.
- Reutilização:se o aplicativo gerenciar bitmaps manualmente, para minimizar a rotatividade da memória, quando um bitmap não for mais necessário, o app deverá chamar
bitmap.recycle()e descartar imediatamente a referênciaBitmap. Se você usar uma biblioteca de carregamento de imagens, como Glide ou Coil, retorne o bitmap para o pool gerenciado da biblioteca. Ao fornecer um buffer existente para necessidades futuras de memória, o pool evita a sobrecarga de novas alocações.
Confira nossa documentação sobre como otimizar o desempenho de imagens para saber mais.
Ferramentas do Android Studio
Você também pode eliminar bitmaps redundantes usando o Android Studio Narwhal 4. Veja como encontrá-los em cinco etapas simples:
- Abra a guia Profiler no Android Studio.
- Clique em Heap Dump (ou "Analisar uso de memória") e pressione "Gravar" para tirar um snapshot do estado atual da memória do app.
- Verifique os resultados da análise em busca do triângulo de aviso amarelo ⚠️, que o Android Studio usa para sinalizar bitmaps duplicados armazenados várias vezes. Outra opção é navegar até o cabeçalho do criador de perfil, escolher "Filtrar por" e selecionar a configuração "Bitmaps duplicados".
- Clique em qualquer entrada sinalizada para abrir o painel Prévia de bitmap e ver exatamente qual imagem é a infratora reincidente.
- Use essa confirmação visual para rastrear a lógica de carregamento redundante no seu código e implementar uma estratégia de cache melhor.
Detectar e corrigir vazamentos de memória com o Android Studio
Os vazamentos de memória no Android ocorrem quando o código mantém uma referência de objeto muito depois do fim do ciclo de vida dele. Isso impede que o coletor de lixo (GC) recupere essa memória, o que pode levar a um desempenho lento ou a um erro OutOfMemory (OOM).
O Android Studio Panda 3 tem uma tarefa de criação de perfil LeakCanary dedicada, permitindo que os desenvolvedores analisem vazamentos de memória em tempo real e mapeiem rastreamentos diretamente no ambiente de desenvolvimento integrado.
A tarefa do criador de perfil do LeakCanary no Android Studio move ativamente a análise de vazamento de memória do dispositivo para a máquina de desenvolvimento, resultando em um aumento significativo de desempenho durante a fase de análise de vazamento em comparação com a análise de vazamento no dispositivo.
Além disso, a análise de vazamento agora é contextualizada no ambiente de desenvolvimento integrado e totalmente integrada ao seu código-fonte, oferecendo recursos como ir para a declaração e outras conexões de código úteis que reduzem drasticamente o atrito e o tempo necessários para investigar e corrigir vazamentos de memória.
Exemplos de vazamentos de memória comuns
Os vazamentos de memória ocorrem quando um objeto permanece na memória além do tempo de vida útil pretendido. Isso geralmente acontece devido a:
- Manter referências a fragmentos, atividades ou visualizações que não estão mais em uso.
- Gerenciamento inadequado de referências de contexto.
- Não cancelar o registro adequado de observadores, listeners e receivers.
- Criar referências estáticas a objetos vinculados a componentes com ciclos de vida mais curtos.
Confira alguns exemplos:
| Cenário | Exemplo baseado no Compose | Exemplo baseado em visualização |
| Vazamento de contexto | Exemplo: Correção: | Exemplo: Correção: |
| Listeners com vazamento | Exemplo: Correção: | Exemplo: Correção: |
| Vazamento de visualizações | Exemplo:
| Exemplo: Correção: |
Reduzir a memória quando o app sai do estado visível
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. Normalmente, o Android recupera a memória do app quando ele não está visível para o usuário, descartando algumas páginas de código e dados do app na memória ou compactando as alocações de heap. Quando o usuário retoma o app e ele tenta acessar alguma memória que foi recuperada, o SO troca essa memória de volta sob demanda. Essa troca pode ser lenta e causar instabilidade ou travamentos inesperados no app.
Se você deixar que o SO decida qual memória recuperar do app, poderá descobrir que o SO recuperou a memória que você precisará logo após retomar o app. Em vez disso, o app pode descartar voluntariamente as alocações de memória que podem ser regeneradas mais tarde, sob demanda e a um baixo custo. Para isso, implemente a interface ComponentCallbacks2. Você pode implementar onTrimMemory nas classes Activity, Fragment, Service ou até mesmo na sua classe Application personalizada. Usá-lo na classe Application é altamente eficaz para o gerenciamento global de cache.
O método de callback onTrimMemory() fornecido notifica seu app sobre eventos relacionados ao ciclo de vida ou à memória que representam uma boa oportunidade para o app reduzir voluntariamente o uso de memória.
Em termos de gerenciamento do ciclo de vida da memória, sua implementação deve se concentrar exclusivamente em TRIM_MEMORY_UI_HIDDEN e TRIM_MEMORY_BACKGROUND. Desde o Android 14, o sistema parou de enviar notificações para outras constantes legadas, que foram formalmente descontinuadas no Android 15.
TRIM_MEMORY_UI_HIDDEN: esse indicador mostra que a interface do aplicativo saiu da visualização do usuário. Isso oferece a 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: nesse nível, seu processo está em segundo plano e agora é um candidato à rescisão para atender às necessidades globais de memória do sistema. Para aumentar a duração em que seu processo permanece no estado armazenado em cache e reduzir o número de inicializações a frio do app, libere de forma agressiva todos os recursos que podem ser facilmente reconstruídos quando o usuário retomar a sessão.
import android.content.ComponentCallbacks2 // Other import statements. class MainActivity : AppCompatActivity(), ComponentCallbacks2 { /** * 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. } } }
Observação: a integração do onTrimMemory pode depender do suporte do SDK. Por exemplo, alguns jogos dependem do mecanismo de jogo para ativar essa capacidade. Confira os documentos de otimização de memória de jogos.
Observabilidade avançada de memória com o ProfilingManager
Para detectar e diagnosticar problemas de memória no campo que não podem ser reproduzidos localmente, use a API ProfilingManager. Introduzida no Android 15, essa API de observabilidade avançada permite coletar programaticamente perfis do Perfetto de usuários reais.
Para equipes que não têm uma infraestrutura dedicada para gerenciar e hospedar artefatos de desempenho, o Crashlytics está desenvolvendo uma solução especializada para simplificar esse fluxo de trabalho. Eles estão convidando os desenvolvedores a enviar feedback.
O Android 17 apresenta novos acionadores orientados por eventos, principalmente TRIGGER_TYPE_OOM e TRIGGER_TYPE_ANOMALY:
- O gatilho de OOM coleta automaticamente um heap dump Java no momento exato em que ocorre uma falha de OutOfMemoryError, fornecendo estados de alocação precisos. Um perfil de OOM coletado é fornecido na próxima vez que o app é iniciado e registra o callback
registerForAllProfilingResults. - O gatilho de anomalia detecta problemas graves de desempenho, como spam excessivo de vinculadores ou violação de limites de memória. A anomalia de memória gera um despejo de heap pouco antes de o sistema encerrar o app.
val profilingManager = applicationContext.getSystemService(ProfilingManager::class.java) val triggers = ArrayList<ProfilingTrigger>() triggers.add(ProfilingTrigger.Builder( ProfilingTrigger.TRIGGER_TYPE_ANOMALY)) val mainExecutor: Executor = Executors.newSingleThreadExecutor() val resultCallback = Consumer<ProfilingResult> { profilingResult -> if (profilingResult.errorCode != ProfilingResult.ERROR_NONE) { // upload profile result to server for further analysis setupProfileUploadWorker(profilingResult.resultFilePath) } profilingManager.registerForAllProfilingResults(mainExecutor, resultCallback) profilingManager.addProfilingTriggers(triggers)
Depois de coletar o heap dump, baixe o perfil do servidor ou localmente usando "adb pull" e arraste e solte o arquivo na interface do Perfetto. Para simplificar seu fluxo de trabalho de depuração de memória, use o Heap Dump Explorer, que é a nova visualização padrão para heap dumps na interface do Perfetto. Essa ferramenta oferece uma interface intuitiva para inspecionar despejos de heap do Java, permitindo visualizar hierarquias de alocação de objetos, calcular tamanhos de memória retidos e identificar o caminho mais curto da raiz da coleta de lixo. Com o Heap Dump Explorer, você pode identificar rapidamente vazamentos de memória, objetos retidos inchados, como alocações excessivas de bitmap, e analisar alocações de objetos de heap em um só lugar.
Conclusão
Otimizar o bytecode com o R8, adotar práticas recomendadas de carregamento de imagens e resolver vazamentos de memória são etapas essenciais para oferecer uma experiência do usuário de alta qualidade e gerenciar recursos de maneira eficaz sob pressão. Adotar essas medidas proativas ajuda a manter a estabilidade e o desempenho do app, evitando encerramentos inesperados e protegendo o contexto do usuário. Para aumentar sua experiência em performance, confira nossas orientações sobre memória revisadas.
-
TutoriaisSabendo que o consumo elevado da bateria é o mais lembrado para os usuários do Android, o Google tem tomado medidas significativas para ajudar os desenvolvedores a criar apps mais eficientes em termos de energia.
Alice Yuan • Leitura de 8 minutos -
TutoriaisO guia de nivelamento de performance tem cinco níveis. Vamos começar com o nível 1, que apresenta ferramentas de desempenho com esforço mínimo de adoção, e vamos até o nível 5, ideal para apps que têm recursos para manter uma estrutura de desempenho personalizada.
Alice Yuan • Leitura de 9 minutos -
TutoriaisAo trabalhar em novos recursos, o desempenho do app geralmente fica em segundo plano. No entanto, embora nem sempre seja a prioridade dos desenvolvedores, os usuários podem ver exatamente onde o desempenho do app fica aquém.
Ben Weiss • Leitura de 3 minutos
Receba os insights mais recentes sobre desenvolvimento Android na sua caixa de entrada semanalmente.