Este documento ajuda a identificar e corrigir os principais problemas de desempenho do seu app.
Principais problemas de desempenho
Há muitos problemas que podem contribuir para o desempenho ruim em um app. Confira alguns problemas comuns:
- Latência de inicialização
A latência de inicialização é o tempo que leva entre o toque no ícone do app, na notificação ou em outro ponto de entrada e a exibição dos dados do usuário na tela.
Estas são as metas de inicialização recomendadas para seus apps:
- Inicialização a frio em menos de 500 ms. Uma inicialização a frio ocorre quando o app que está sendo iniciado não está presente na memória do sistema. Isso acontece quando é a primeira inicialização do app desde a reinicialização ou desde que o processo do app foi interrompido pelo usuário ou pelo sistema. Uma inicialização a frio exige o máximo de trabalho do sistema, já que ela precisa carregar tudo do armazenamento e inicializar o app. Tente programar a inicialização a frio com uma duração de até 500 ms.
- Inicialização com estado salvo em menos de 200 ms e inicialização a quente em menos de 150 ms. Uma inicialização lenta ocorre quando o processo do aplicativo já está em execução em segundo plano, mas o sistema precisa reinicializar a interface ou trazer a atividade de volta ao primeiro plano, como quando um usuário sai do app e o abre novamente logo depois. Uma inicialização a quente é ainda mais rápida porque a atividade do app já está armazenada em cache na memória e só precisa ser trazida para o primeiro plano, sem a necessidade de recriar a hierarquia de visualização. Mantenha as inicializações a quente abaixo de 200 ms e as inicializações a frio abaixo de 150 ms.
- Latências P95 e P99 muito próximas à latência mediana. P95 e P99 representam o 95º e o 99º percentis dos tempos de inicialização, enquanto a mediana é o 50º percentil. Quando o app demora muito para iniciar, ele prejudica a experiência do usuário. As comunicações entre processos (IPCs) e a E/S desnecessária durante o caminho crítico de inicialização do app podem enfrentar contenção de bloqueios e introduzir inconsistências.
- Instabilidade de rolagem
Instabilidade é o termo que descreve o problema visual que ocorre quando o sistema não consegue criar e fornecer frames a tempo de direcioná-los à tela na cadência solicitada de 60 hz ou mais. A instabilidade é mais aparente na rolagem, quando, em vez de um fluxo animado suave, podem ocorrer falhas. A instabilidade aparece quando o movimento é pausado ao longo do caminho por um ou mais frames, já que o app leva mais tempo para renderizar o conteúdo do que a duração de um frame no sistema.
Defina taxas de atualização de 90 Hz nos seus apps. As taxas de renderização convencionais são de 60 Hz, mas muitos dispositivos mais recentes operam no modo 90 Hz durante interações do usuário, como a rolagem. Alguns dispositivos oferecem suporte a taxas ainda mais altas de até 120 hz.
Para conferir a taxa de atualização usada por um dispositivo em um determinado momento, ative uma sobreposição em Opções do desenvolvedor > Mostrar taxa de atualização na seção Depuração.
- Transições instáveis
Isso é aparente durante interações, como alternar entre guias ou carregar uma nova atividade. Esses tipos de transição precisam ter animação suave, sem incluir atrasos ou oscilação visual.
- Ineficiências de energia
Atividades reduzem a carga da bateria e, consequentemente, a duração dela.
As alocações de memória, provenientes da criação de novos objetos no código, causam trabalho no sistema. Isso ocorre não só porque as alocações exigem esforço do Android Runtime (ART), mas liberar esses objetos mais tarde (coleta de lixo) também exige tempo e esforço.
A alocação de memória e a coleta de lixo ficaram muito mais rápidas e eficientes, principalmente para objetos temporários. Embora fosse uma prática recomendada evitar alocar objetos sempre que possível, sugerimos que você faça o que fizer mais sentido para seu app e sua arquitetura. Salvar em alocações com o risco de um código sem manutenção não é a prática recomendada, considerando os recursos do ART.
No entanto, as alocações exigem esforço. Portanto, lembre-se de que elas podem contribuir para problemas de desempenho se você estiver alocando muitos objetos no loop interno.
Identificar problemas
Para corrigir problemas de desempenho, identifique e inspecione as seguintes jornadas ideais do usuário:
- Fluxos de inicialização comuns, incluindo aqueles da tela de início e notificação.
- Telas em que o usuário rola os dados.
- Transições entre telas.
- Fluxos de longa duração, como navegação ou reprodução de música.
Para cada um desses fluxos, inspecione o que está acontecendo usando as seguintes ferramentas de depuração:
- Perfetto: permite conferir o que está acontecendo em todo o dispositivo com dados de tempo precisos.
- Memory Profiler: permite conferir quais alocações de memória estão acontecendo no heap.
- Simpleperf: mostra um diagrama das chamadas de função que usam mais CPU durante um determinado período. Quando você identifica algo que está demorando muito no Systrace, mas não sabe o motivo, o Simpleperf pode fornecer mais informações.
Para entender e depurar esses problemas de desempenho, é fundamental depurar manualmente as execuções de teste individuais. Não é possível substituir as etapas anteriores analisando dados agregados. No entanto, para entender o que é realmente mostrado aos usuários e identificar quando podem ocorrer regressões, é importante configurar a coleta de métricas em testes automatizados e de campo:
- Fluxos de inicialização
- Métricas de campo: tempo de inicialização do Play Console
- Testes de laboratório: inicialização de testes com a biblioteca Macrobenchmark.
- Instabilidade
- Métricas de campo
- Frames vitais no Play Console: não é possível restringir as métricas a uma jornada do usuário específica no Play Console. Ele só relata a instabilidade geral do app.
- Medição personalizada com
FrameMetricsAggregator: é possível usarFrameMetricsAggregatorpara registrar métricas de instabilidade durante um fluxo de trabalho específico.
- Testes de laboratório
- Rolagem com a biblioteca Macrobenchmark.
- O MacroBenchmark coleta o tempo para a renderização do frame usando os comandos
dumpsys gfxinfoque formam uma única jornada do usuário. Essa é uma maneira de entender a variação na instabilidade em uma jornada específica. As métricasRenderTime, que destacam o tempo que os frames estão demorando para carregar, são mais importantes que a contagem de frames instáveis para identificar regressões ou melhorias.
- Métricas de campo
Problemas de verificação de links de apps
Os Links do app são links diretos baseados no URL do site que foram verificados como pertencentes a ele. As verificações de Link do app podem falhar pelos seguintes motivos:
- Escopos incorretos de filtro de intent:adicione apenas
autoVerifyaos filtros de intent para URLs que seu app pode responder. - Trocas de protocolo não verificadas:redirecionamentos não verificados do lado do servidor e de subdomínio
são considerados riscos de segurança e falham na verificação. Elas causam a falha de todos os links do
autoVerify. Por exemplo, redirecionar links de HTTP para HTTPS, como example.com para www.example.com, sem verificar os links HTTPS pode causar falha na verificação. Verifique os links de apps adicionando filtros de intent. - Links não verificáveis:adicionar links não verificáveis para fins de teste pode fazer com que o sistema não verifique os links de apps para seu app.
- Servidores não confiáveis:verifique se os servidores podem se conectar aos apps clientes.
Configurar o app para análise de desempenho
É essencial fazer a configuração correta para conseguir comparações precisas, repetíveis e acionáveis de um app. Faça o teste em um sistema o mais próximo possível da produção, suprimindo fontes de ruído. As próximas seções mostram uma série de etapas específicas do APK e do sistema que você pode seguir para preparar uma configuração de teste. Algumas delas são específicas para determinados caso de uso.
Pontos de rastreamento
Os apps podem instrumentar o código com eventos de rastreamento personalizados.
Enquanto os rastros estão sendo capturados, o rastreamento incorre em uma pequena sobrecarga (aproximadamente 5 μs) por seção. Portanto, não o coloque em cada método. O rastreamento de blocos de trabalho maiores, >0,1 ms, pode fornecer insights significativos sobre gargalos.
Considerações sobre o APK
As variantes de depuração podem ser úteis para solucionar problemas e simbolizar amostras de pilha,
mas têm impactos graves no desempenho. Dispositivos com o Android 10 (nível 29 da API) e mais recentes podem usar profileable android:shell="true"
no manifesto para ativar a criação de perfis em builds de lançamento.
Use sua configuração de redução de código no nível da produção. Dependendo dos recursos usados pelo app, isso pode ter um impacto significativo no desempenho. Algumas configurações do ProGuard removem pontos de rastreamento. Por isso, considere remover essas regras para a configuração em que os testes serão feitos.
Compilação
Compile seu app no dispositivo para um estado conhecido, geralmente speed para
simplicidade ou speed-profile para corresponder mais de perto ao desempenho de produção
(embora isso exija o aquecimento do aplicativo e o despejo de perfis ou
a compilação dos perfis de referência do app).
Tanto speed quanto speed-profile reduzem a quantidade de código interpretado em execução
do dex e, consequentemente, a quantidade de compilação just-in-time (JIT)
em segundo plano, o que pode causar interferências significativas. Somente speed-profile
reduz o impacto do carregamento de classes de tempo de execução do dex.
O comando a seguir compila o aplicativo usando o modo speed:
adb shell cmd package compile -m speed -f com.example.packagename
O modo de compilação speed compila totalmente os métodos do app. O modo
speed-profile compila os métodos e as classes do app de acordo com um
perfil dos caminhos de código utilizados coletado durante o uso do app. Pode ser
difícil coletar perfis de forma consistente e correta. Portanto, se você optar por
usá-los, confirme se estão coletando o que você espera. Os perfis estão localizados
neste local:
/data/misc/profiles/ref/[package-name]/primary.prof
Considerações do sistema
Para medidas de baixo nível e alta fidelidade, calibre seus dispositivos. Execute comparações A/B no mesmo dispositivo e na mesma versão do SO. Podem existir variações significativas no desempenho, mesmo em dispositivos do mesmo tipo.
Em dispositivos com acesso root, use um script lockClocks para
microcomparativos. Entre outras coisas, esses scripts podem fazer o seguinte:
- colocar CPUs com uma frequência fixa;
- desativar núcleos pequenos e configurar a GPU;
- desativar a limitação térmica.
Não recomendamos o uso de um script lockClocks para testes focados na experiência do usuário,
como inicialização do app, testes DoU e testes de instabilidade, mas ele pode ser essencial para
reduzir o ruído em testes de Microbenchmark.
Sempre que possível, use um framework de teste como o Macrobenchmark, que pode reduzir o ruído nas medições e evitar imprecisão delas.
Inicialização lenta do app: atividade trampolim desnecessária
Uma atividade trampolim pode estender o tempo de inicialização do app desnecessariamente, e é
importante saber se o app está fazendo isso. Conforme mostrado no exemplo de rastro
abaixo, uma activityStart é imediatamente seguido por outra activityStart,
sem que nenhum frame seja renderizado pela primeira atividade.
Figura 1. Um rastro mostrando uma atividade trampolim.
Isso pode acontecer em um ponto de entrada de notificação ou em um ponto de entrada normal de inicialização do app, e muitas vezes é possível resolver o problema com a refatoração. Por exemplo, se você estiver usando essa atividade para fazer a configuração antes de executar outra atividade, fatore o código em um componente ou biblioteca reutilizável.
Alocações desnecessárias acionam GCs frequentes
Coletas de lixo (GCs, na sigla em inglês) podem acontecer com mais frequência que o esperado em um Systrace.
No exemplo a seguir, a coleta de lixo a cada 10 segundos durante uma operação de longa duração é um indicador de que o app pode estar alocando de forma desnecessária, mas consistente ao longo do tempo:
Figura 2. Um rastro mostrando espaço entre eventos de GC.
No Memory Profiler, você também pode notar que uma pilha de chamadas específica está fazendo a grande maioria das alocações. Você não precisa eliminar todas as alocações de forma agressiva, já que isso pode dificultar a manutenção do código. Em vez disso, comece trabalhando em pontos de acesso de alocações.
Frames com instabilidade
O pipeline de gráficos é relativamente complicado, e pode haver algumas nuances para determinar se um usuário pode perceber um frame descartado. Em alguns casos, a plataforma pode "resgatar" um frame usando armazenamento em buffer. No entanto, você pode ignorar a maior parte dessas nuances para identificar facilmente frames problemáticos da perspectiva do app.
Quando os frames estão sendo exibidos com pouco trabalho do app, os
pontos de rastreamento Choreographer.doFrame() ocorrem em uma cadência de 16,7 ms em um dispositivo de
60 QPS:
Figura 3. Um rastro mostrando frames frequentes.
Se você diminuir o zoom e navegar pelo rastro, vai notar que os frames demoram um pouco para serem concluídos, mas não mais que 16,7 ms. Estes frames estão OK:
Figura 4. Um rastro mostrando frames rápidos frequentes com bursts de trabalho
periódicos.
Quando há uma interrupção nessa cadência regular, o problema é um frame instável, como mostrado nas figuras 5 e 6:
Figura 5. Um rastro mostrando um frame instável.
Figura 6. Um rastro mostrando mais frames instáveis.
Em alguns casos, você precisa aumentar o zoom em um ponto de rastreamento para conferir mais informações sobre
quais componentes de UI estão sendo atualizados pelo Compose ou, como na Figura 6, o que
um LazyColumn está fazendo. Ao diagnosticar esses gargalos da interface, o rastreamento padrão do sistema pode não mostrar quais elementos combináveis são a causa raiz. Nesses casos, use o rastreamento de composição do Jetpack Compose, que mostra funções combináveis exatas diretamente no rastreamento, facilitando a identificação de recomposições inesperadas. As figuras 5 e 6 mostram os resultados do rastreamento de composição.
Para mais informações sobre como otimizar a performance do Compose, consulte Performance do Jetpack Compose. Para saber mais sobre como identificar frames instáveis e depurar os motivos, consulte Renderização lenta.
Erros comuns de layout lento
Invalidar todo o estado de apoio de um layout lazy desnecessariamente pode levar a recomposições excessivas, longos tempos de renderização de frame e instabilidade. Para minimizar o número de itens da lista que precisam ser atualizados, use chaves de item para seus itens e mude apenas os elementos de estado específicos que mudam.
Consulte Usar chaves de layout lentas para evitar realocações caras de listas completas, que fazem com que o conteúdo seja atualizado em vez de totalmente substituído.
A implementação inadequada de listas de rolagem aninhadas pode causar quedas de desempenho. Evite aninhar um layout lazy de rolagem dentro de outro contêiner de rolagem sem restrições explícitas. Para mais informações, consulte Evitar o aninhamento de componentes roláveis na mesma direção.
Não fazer a pré-busca de dados suficientes ou não fazer a pré-busca em tempo hábil pode tornar o acesso ao fim de uma lista de rolagem desagradável quando o usuário precisa esperar mais dados do servidor. Embora isso não seja tecnicamente uma instabilidade, já que nenhum prazo de frame está sendo perdido, é possível melhorar significativamente a UX modificando o tempo e a quantidade de pré-busca para que o usuário não tenha que esperar os dados.
Depurar seu app
Confira a seguir alguns métodos para depurar a performance do seu app.
Depurar a inicialização do app com o Systrace
Consulte Tempo de inicialização do app para ter uma visão geral do processo de inicialização do app e assista ao vídeo a seguir para ter uma visão geral do rastreamento do sistema e do uso do Android Studio Profiler.
É possível desambiguar os tipos de inicialização nas seguintes etapas:
- Inicialização a frio: começa criando um novo processo sem estado salvo.
- Inicialização com estado salvo: recria a atividade reutilizando o processo ou recria o processo com o estado salvo.
- Inicialização a quente: reinicia a atividade e começa na inflação.
Recomendamos capturar systraces com o app System Tracing no dispositivo. No Android 10 e em versões mais recentes, use o Perfetto. No Android 9 e versões anteriores, use o Systrace. Recomendamos também visualizar arquivos de rastreamento com o leitor de rastros do Perfetto baseado na Web. Para mais informações, consulte Visão geral do rastreamento do sistema.
Alguns pontos a serem observados:
- Disputa de monitor: a concorrência por recursos protegidos pelo monitor pode introduzir um atraso significativo na inicialização do app.
Transações de binder síncronas: procure transações desnecessárias no caminho crítico do seu app. Se uma transação necessária for cara, trabalhe com a equipe da plataforma associada para fazer melhorias.
Coleta de lixo simultânea: é comum e tem impacto relativamente baixo, mas se ela estiver acontecendo com frequência, investigue-a com o Memory Profiler do Android Studio.
E/S: verifique a E/S realizada durante a inicialização e procure paradas longas.
Atividades significativas em outras linhas de execução: elas podem interferir na linha de execução de interface. Preste atenção no trabalho em segundo plano durante a inicialização.
Recomendamos que você chame reportFullyDrawn quando a inicialização for concluída do
ponto de vista do app para melhorar os relatórios de métricas de inicialização do app. Consulte a seção Tempo
para exibição completa para mais informações sobre como usar reportFullyDrawn.
É possível extrair os horários de início definidos pelo RFD usando o processador de rastreamento do Perfetto, e um evento de rastreamento visível para o usuário é emitido.
Usar o rastreamento do sistema no dispositivo
Você pode usar o app no nível do sistema chamado "Rastreamento do sistema" para capturar um rastreamento do sistema em um dispositivo. Com ele, é possível gravar rastros do dispositivo sem
precisar conectá-lo ou ligá-lo ao adb.
Usar o Memory Profiler do Android Studio
Use o Memory Profiler do Android Studio para inspecionar a pressão da memória que pode ser causada por vazamentos de memória ou padrões de uso inadequados. Ele oferece uma visualização ao vivo das alocações de objetos.
Para corrigir problemas de memória no app, use o Memory Profiler para acompanhar o motivo e a frequência das GCs.
Para criar um perfil de memória do app, siga estas etapas:
Detectar problemas de memória.
Grave uma sessão de criação de perfil de memória da jornada do usuário em que você quer se concentrar. Procure um aumento na contagem de objetos, como mostrado na Figura 7, que eventualmente leva a GCs, como mostrado na Figura 8.
Figura 7. Contagem crescente de objetos.
Figura 8. Coletas de lixo.Depois de identificar a jornada do usuário que está aumentando a pressão sobre a memória, analise as causas raiz.
Diagnosticar pontos de acesso de pressão sobre a memória.
Selecione um intervalo na linha do tempo para visualizar Alocações e Tamanho superficial, conforme mostrado na figura 9.
Figura 9. Valores para Allocations e Shallow
Size.Há várias maneiras de ordenar esses dados. Confira a seguir alguns exemplos de como cada visualização pode ajudar você a analisar problemas.
Organizar por classe: útil quando você quer encontrar classes que estão gerando objetos que, de outra forma, são armazenados em cache ou reutilizados de um pool de memória.
Por exemplo, se um app estiver criando 2 mil objetos de uma classe específica a cada segundo, isso aumentará a contagem de Alocações em 2 mil a cada segundo, e você verá isso ao classificar por classe. Se você quiser reutilizar esses objetos para evitar a geração de lixo, implemente um pool de memória.
Organizar por pilha de chamadas: útil quando você quer encontrar um caminho em que a memória está sendo alocada, como dentro de um loop ou de uma função específica que realiza grande parte da alocação.
Shallow Size: rastreia apenas a memória do próprio objeto. É útil para rastrear classes simples compostas principalmente de valores primitivos.
Tamanho retido: mostra a memória total devido ao próprio objeto mais quaisquer referências que são apenas referenciadas pelo objeto. Ele é útil para rastrear a pressão sobre a memória causada por objetos complexos. Para conseguir esse valor, faça um despejo de memória completo, conforme mostrado na Figura 10. Tamanho retido é adicionado como uma coluna, conforme mostrado na Figura 11.
Figura 10. Despejo de memória completo.
Figura 11. Coluna "Tamanho retido".
Medir o impacto de uma otimização.
Os GCs são mais evidentes e facilitam a medição do impacto das otimizações na memória. Quando uma otimização reduz a pressão sobre a memória, você vê menos GCs.
Para medir o impacto da otimização, na linha do tempo do profiler, meça o tempo entre as GCs. O impacto positivo resulta em mais tempo entre as GCs.
Veja os impactos finais das melhorias de memória:
- Os encerramentos por falta de memória provavelmente serão reduzidos se o app não atingir constantemente a pressão da memória.
- A redução de GCs melhora as métricas de instabilidade, principalmente no P99. Isso ocorre porque as GCs causam contenção de CPU, o que pode adiar tarefas de renderização durante a GC.
Recomendados para você
- Observação: o texto do link aparece quando o JavaScript está desativado
- Otimização e análise de inicialização do app {:#app-startup-analysis-optimization}
- Frames congelados
- Como criar uma comparação da Macrobenchmark