Resposta rápida: use o Android Studio Profiler para observar o app enquanto reproduz um problema e escolher a investigação certa: CPU para métodos lentos, threads ocupadas e jank; memória para alocações, heap dumps e objetos retidos; rede para requisições, payloads e latência; e System Trace para relacionar trabalho do app com frames, scheduler e sistema operacional. Comece por um cenário curto e repetível, marque o intervalo do problema, formule uma hipótese e compare antes e depois da correção no mesmo aparelho e variante. O Profiler ajuda a localizar gargalos, mas não substitui benchmarks controlados, testes em celulares representativos nem métricas reais de produção.

Quando uma tela Android “parece lenta”, tentar otimizar olhando apenas para o código costuma gerar mudanças sem efeito. O atraso pode vir de uma consulta no banco, desserialização excessiva, recomposição, imagem grande, coleta de lixo, trabalho na main thread, resposta de rede ou inicialização de uma biblioteca. Sem medir, todos esses candidatos parecem plausíveis.

O Android Studio Profiler reúne ferramentas para transformar essa sensação em evidência. Ele permite acompanhar a atividade do processo, capturar traces, inspecionar memória e analisar tráfego compatível. A meta não é deixar todos os gráficos baixos o tempo inteiro. A meta é responder uma pergunta concreta, como “por que o scroll trava ao carregar a próxima página?” ou “por que a memória cresce toda vez que abro e fecho esta tela?”.

Neste guia, você vai montar uma sessão de profiling confiável, escolher entre CPU, memória e rede, interpretar sinais sem tirar conclusões precipitadas e validar uma correção. Para problemas encontrados ainda durante o desenvolvimento, combine o fluxo com StrictMode no Android e LeakCanary com Kotlin.

O que o Android Studio Profiler mede?

O Profiler não é uma única medição. Ele funciona como uma bancada com instrumentos diferentes, cada um adequado a uma família de perguntas.

FerramentaPergunta principalSinais úteis
CPU Profileronde o app gasta tempo de processamento?métodos, call stacks, threads e traces
System Traceo que acontecia no app e no sistema durante o atraso?frames, scheduler, threads e eventos do sistema
Memory Profilerquais objetos são alocados ou continuam retidos?heap, classes, referências e atividade do GC
Network Inspectorqual requisição está lenta, grande ou incorreta?URL, duração, headers, body e sequência temporal
Energy Profiler, quando disponívelquais ações podem pressionar bateria?rede, localização, jobs, alarms e uso de CPU

A disponibilidade e a aparência exata dessas ferramentas variam conforme a versão do Android Studio, o dispositivo, o nível da API e o tipo de processo. Portanto, trate nomes de botões como detalhes da interface. O raciocínio permanece: conecte um processo, reproduza uma ação, capture o intervalo relevante e investigue uma hipótese.

Também é importante distinguir profiling de benchmark. O Profiler é excelente para explorar e diagnosticar. Um Macrobenchmark com Baseline Profiles é melhor para comparar métricas de startup ou frames em execuções controladas e repetidas. Usar os dois fecha o ciclo: o Profiler ajuda a descobrir a causa; o benchmark ajuda a provar que a mudança melhorou o cenário.

Como preparar uma sessão confiável

Uma sessão ruim produz gráficos bonitos e conclusões frágeis. Antes de abrir qualquer ferramenta, escreva o cenário em uma frase:

Abrir a tela de catálogo, esperar o carregamento inicial e rolar até a terceira página em um aparelho intermediário.

Depois, controle as condições que mais alteram o resultado:

  1. use sempre o mesmo aparelho ou emulator durante a comparação;
  2. mantenha a mesma variante e configuração de build;
  3. decida se o cenário começa com processo frio, morno ou já aberto;
  4. restaure os mesmos dados locais e estado de login;
  5. controle cache e qualidade da conexão quando a rede fizer parte do teste;
  6. repita a ação algumas vezes antes de concluir;
  7. registre o commit, aparelho, Android e passos usados.

A build debug facilita inspeção, mas pode ter overhead, ferramentas adicionais e comportamento diferente do release. Para investigar um problema exclusivo de produção — por exemplo, depois da minificação — use uma variante profileable ou interna que se aproxime do release sem expor credenciais de assinatura. O artigo sobre R8 e regras ProGuard explica por que debug e release podem divergir.

Evite comparar uma execução debug com uma release e atribuir toda diferença a uma única mudança de código. Compare variantes equivalentes e altere uma condição por vez.

Conectando o app ao Profiler

Abra o projeto no Android Studio, execute o app no dispositivo e acesse a janela de ferramentas do Profiler. Selecione o dispositivo e o processo correto. Em projetos com processos separados, como um serviço declarado com android:process, confirme que você não está observando apenas o processo principal.

Antes de capturar, faça uma passagem manual pelo cenário. Isso confirma que:

  • a conta e os dados de teste estão prontos;
  • a tela realmente reproduz o problema;
  • não há diálogo de permissão bloqueando o fluxo;
  • o processo selecionado é o correto;
  • a sessão não inclui minutos de ruído antes da ação importante.

Faça capturas curtas. Um trace de alguns segundos envolvendo uma interação específica costuma ser mais útil que uma gravação enorme do app inteiro. Capturas longas aumentam custo, volume e dificuldade de navegação.

CPU Profiler: encontrando trabalho caro

Use o CPU Profiler quando o sintoma envolver:

  • toque que demora a responder;
  • scroll com engasgos;
  • animação irregular;
  • transformação de dados lenta;
  • uso alto de CPU;
  • startup com inicialização pesada;
  • tarefa em background que termina tarde demais.

A primeira pergunta não é “qual método tem o maior tempo total?”. É “qual trabalho coincide com o intervalo em que o usuário percebeu o atraso?”. Selecione esse intervalo e então examine threads, call stacks e métodos.

Diferencie tempo próprio e tempo total

Um método pode aparecer caro porque chamou várias funções lentas. O tempo total inclui o trabalho dos descendentes; o tempo próprio representa o que ocorreu no corpo daquele método, conforme o tipo de captura.

Considere este exemplo:

fun loadCatalog(): CatalogUiState {
    val rows = repository.queryAll()
    val products = rows.map(mapper::toDomain)
    val sorted = products.sortedBy { it.displayName }
    return CatalogUiState(products = sorted)
}

Se loadCatalog() domina o tempo total, a causa pode estar em queryAll(), no mapper ou na ordenação. Não reescreva a função inteira antes de expandir a árvore de chamadas.

Procure trabalho na main thread

A main thread deve coordenar eventos e renderização, não executar I/O bloqueante nem transformação pesada. Durante o intervalo lento, procure blocos longos nessa thread e relacione-os à stack.

Um erro comum em Kotlin é acreditar que toda coroutine já executa fora da main thread:

viewModelScope.launch {
    val payload = file.readText() // leitura bloqueante ainda pode ocorrer na main
    _state.value = parse(payload)
}

viewModelScope.launch usa a main thread por padrão. Operações bloqueantes precisam de uma fronteira adequada:

viewModelScope.launch {
    val result = withContext(Dispatchers.IO) {
        val payload = file.readText()
        parse(payload)
    }
    _state.value = result
}

Isso não significa mover qualquer função automaticamente para Dispatchers.IO. Trabalho intensivo de CPU pode merecer Dispatchers.Default; APIs assíncronas talvez não precisem de troca manual; e criar dispatches pequenos demais adiciona complexidade. Use o trace para identificar a natureza e o tamanho do trabalho.

Sampling ou instrumentação?

Capturas por sampling observam stacks em intervalos e tendem a introduzir menos overhead. São boas para localizar hotspots em uma interação. Capturas instrumentadas registram entrada e saída de métodos com mais detalhe, mas podem alterar de forma mais significativa o comportamento observado e gerar muito volume.

Comece com a opção menos intrusiva que responda à pergunta. Se o resultado não tiver detalhe suficiente, aprofunde a captura em um cenário menor. Nunca trate duração observada sob instrumentação pesada como se fosse o tempo exato percebido pelo usuário em produção.

System Trace: entendendo jank e concorrência

Quando o problema envolve frames, contenção entre threads, scheduler ou interação com o sistema, o System Trace oferece contexto que uma lista de métodos isolada não mostra.

Ele é particularmente útil para investigar:

  • frames demorados durante scroll ou animação;
  • main thread ocupada no momento de um toque;
  • RenderThread ou operações gráficas;
  • threads que ficam bloqueadas ou esperando;
  • execução concorrente demais;
  • startup distribuído entre app e sistema;
  • garbage collection coincidindo com travamentos visuais.

Ao analisar jank, não conclua que “CPU alta” é sempre o problema. Uma thread pode estar bloqueada esperando lock, binder, disco ou resultado de outra thread. Da mesma forma, um pico curto pode ser aceitável se não atrasar o frame nem a interação.

Para facilitar a leitura, nomeie threads e operações importantes e adicione marcadores de trace em caminhos críticos quando apropriado. O objetivo é conseguir reconhecer “carregar catálogo”, “decodificar imagem” ou “montar feed” no meio de eventos do sistema, sem espalhar instrumentação permanente por todo método pequeno.

Em interfaces Compose, use também ferramentas específicas de recomposição e inspeção quando a hipótese for UI. O Profiler pode mostrar onde o tempo foi gasto, mas uma análise do estado e da estabilidade dos parâmetros explica por que uma composição aconteceu. O guia de Jetpack Compose oferece a base para revisar estado e ciclo de recomposição.

Memory Profiler: alocação não é automaticamente leak

Use o Memory Profiler quando o sintoma for:

  • memória crescendo a cada navegação;
  • OutOfMemoryError;
  • coleta de lixo frequente;
  • imagens ou buffers ocupando espaço demais;
  • Activity, Fragment ou View que não é liberado;
  • cache sem limite;
  • lista grande materializada desnecessariamente.

O gráfico mostra mudanças no uso de memória e eventos como garbage collection. Entretanto, uma linha crescente por alguns segundos não prova vazamento. Apps alocam objetos para executar trabalho, caches podem crescer até um limite e o runtime decide quando coletar.

Um teste melhor para retenção segue um ciclo repetível:

  1. abra a tela suspeita;
  2. execute a ação que cria os objetos;
  3. volte e descarte a tela;
  4. repita o ciclo algumas vezes;
  5. force uma coleta apenas como auxílio de diagnóstico, sabendo que isso muda o cenário;
  6. capture o heap e procure instâncias que deveriam ter desaparecido.

Como ler um heap dump

No heap dump, filtre pela classe suspeita e observe:

  • quantidade de instâncias;
  • shallow size do próprio objeto;
  • retained size estimado;
  • referências de entrada;
  • caminho até uma GC root.

O caminho de retenção é mais importante que o tamanho isolado. Uma Activity pode ser pequena, mas manter uma árvore inteira de Views, drawables e dados. Procure a primeira referência com ciclo de vida longo que não deveria apontar para o objeto curto.

Exemplo clássico:

object AnalyticsBridge {
    var currentActivity: Activity? = null
}

Mesmo que a tela termine, o singleton continua vivo e segura a Activity. A correção não é “pedir mais GC”; é remover a referência, usar um contexto de aplicação quando adequado ou redesenhar o contrato para não armazenar a tela.

Alocações excessivas e pressão do GC

Nem todo problema de memória é leak. Um mapper que cria milhares de objetos em cada frame pode causar pressão de coleta e jank mesmo que tudo seja liberado corretamente.

Observe alocações durante uma ação curta e procure classes criadas em volume inesperado. Em listas, verifique conversões repetidas, formatação dentro de recomposições, cópias grandes de coleções e decodificação de imagens fora do tamanho necessário.

Não elimine objetos idiomáticos de Kotlin por reflexo. data class, lambdas e coleções são ferramentas legítimas. Otimize apenas um caminho medido, preserve legibilidade e confirme o resultado depois.

Para objetos realmente retidos, o guia de LeakCanary complementa o heap dump com detecção automática e leak traces durante o desenvolvimento.

Network Inspector: latência, payload e sequência

Use o Network Inspector quando uma tela espera dados, baixa payload grande, repete chamadas ou apresenta resultado diferente do esperado. Dependendo da biblioteca e da configuração do app, a ferramenta consegue mostrar informações de tráfego compatível, incluindo:

  • URL e método HTTP;
  • status da resposta;
  • início e duração;
  • headers;
  • request e response body;
  • ordem e sobreposição das requisições.

A visão temporal costuma revelar problemas arquiteturais rapidamente. Uma tela pode fazer cinco chamadas em série que poderiam ser paralelas, repetir a mesma busca a cada recomposição ou baixar um JSON muito maior que o necessário.

Considere um campo de busca em Compose:

LaunchedEffect(query) {
    repository.search(query)
}

Se query muda a cada tecla, o app pode disparar uma requisição por caractere. O Network Inspector torna essa sequência visível. Uma solução com Flow pode aplicar normalização, debounce, distinctUntilChanged e cancelamento da busca anterior:

val results = queryFlow
    .map(String::trim)
    .debounce(350)
    .distinctUntilChanged()
    .filter { it.length >= 2 }
    .flatMapLatest(repository::search)

Os valores exatos dependem do produto; não transforme 350 ms em regra universal. O ponto é reduzir trabalho sem tornar a interface lenta e cancelar respostas obsoletas.

Cuidado com dados sensíveis

Payloads podem conter tokens, dados pessoais, IDs e conteúdo privado. Use contas e ambientes de teste, não compartilhe capturas sem revisão e não coloque credenciais reais em screenshots, commits ou tickets. Em produção, respeite a Lei Geral de Proteção de Dados (LGPD) e a política de observabilidade da empresa.

O Inspector também não representa sozinho a experiência de usuários em redes móveis variadas. Teste latência, perda, modo offline e retomada. Para construir uma experiência resistente, consulte o guia de arquitetura offline-first com Kotlin.

Um fluxo prático de investigação

Um processo simples evita a chamada “otimização por superstição”.

1. Defina o sintoma observável

Ruim: “o app está pesado”.

Bom: “ao voltar do detalhe para a lista, os primeiros dois segundos de scroll apresentam travamentos”.

2. Escolha uma métrica ou sinal

Para esse exemplo, capture frames e atividade da main thread. Se a memória também cresce a cada volta, abra uma investigação separada. Misturar cinco hipóteses na mesma sessão dificulta provar qualquer uma.

3. Reproduza com o menor cenário possível

Inicie a captura pouco antes da ação e pare logo depois. Anote o instante do toque ou use um marcador reconhecível.

4. Encontre a correlação

Verifique qual thread estava ativa, qual stack dominou o intervalo e se houve GC, I/O ou espera. Correlação não é ainda causalidade, mas reduz o espaço de busca.

5. Formule uma hipótese falsificável

Exemplo: “a lista trava porque cada item formata uma imagem e ordena tags novamente durante a renderização”.

6. Faça uma mudança pequena

Mova a transformação para a camada de estado, reutilize o resultado e mantenha o restante igual. Mudanças amplas impedem saber qual parte ajudou.

7. Repita nas mesmas condições

Capture novamente, compare o mesmo intervalo e execute mais de uma vez. Depois transforme o cenário em benchmark ou teste de regressão quando o custo justificar.

Erros comuns ao usar o Profiler

Otimizar o maior pico sem relacioná-lo ao usuário

Um pico pode ocorrer durante uma ação esperada e fora do caminho crítico. Priorize o que afeta startup, frame, toque, bateria ou uso real.

Comparar aparelhos e builds diferentes

Resultados de hardware, Android, temperatura, cache, rede e variante não são diretamente comparáveis. Registre o ambiente.

Gravar tempo demais

Capturas gigantes dificultam análise e podem aumentar overhead. Reduza o cenário.

Confundir alocação com vazamento

Objetos novos são normais. Leak exige retenção indevida depois que o ciclo de vida deveria terminar.

Mover tudo para outra thread

Isso pode esconder uma violação e criar contenção, ordem incorreta ou atualização fora do ciclo de vida. Identifique se o trabalho é I/O, CPU, API assíncrona ou renderização.

Buscar zero uso de CPU e memória

Um app útil consome recursos. A meta é custo proporcional, previsível e dentro do orçamento do produto.

Ignorar release e dispositivos modestos

Um celular topo de linha conectado por cabo mascara problemas. Valide em hardware representativo e em uma build próxima da publicada.

Checklist antes de encerrar a análise

  • O cenário foi descrito em passos reproduzíveis?
  • A captura cobre somente o intervalo relevante?
  • O processo e a variante estão corretos?
  • A hipótese aponta uma causa, não apenas um gráfico?
  • A mudança foi pequena o suficiente para comparação?
  • O teste depois da correção usou o mesmo ambiente?
  • O resultado foi repetido mais de uma vez?
  • Existe benchmark ou teste que pode impedir regressão?
  • A captura foi revisada para remover dados sensíveis?
  • O problema também precisa de telemetria de produção?

Profiler, StrictMode, LeakCanary ou Macrobenchmark?

As ferramentas se complementam:

NecessidadeFerramenta inicial
detectar disco ou rede na main thread durante desenvolvimentoStrictMode
encontrar objetos Android retidosLeakCanary e Memory Profiler
localizar métodos, threads e intervalos carosCPU Profiler e System Trace
inspecionar sequência e conteúdo de requisições compatíveisNetwork Inspector
comparar startup e frames de forma repetívelMacrobenchmark
observar crashes e ANR em usuários reaisCrashlytics e Android vitals

Para produção, use sinais agregados e respeite privacidade. O guia de Crashlytics e ANR cobre a passagem entre um problema observado por usuários e uma reprodução técnica no laboratório.

Perguntas frequentes

O Android Studio Profiler deixa o app mais lento?

Pode deixar. Capturas e recursos de depuração introduzem overhead, especialmente em modos instrumentados. Use o Profiler para diagnóstico e compare performance quantitativa com benchmarks controlados em uma variante apropriada.

CPU alta significa que existe um bug?

Não necessariamente. Processar uma imagem, compilar código, montar uma tela ou executar criptografia consome CPU. O problema aparece quando o custo é inesperado, prolongado, repetitivo ou atrasa uma interação importante.

Como saber se o crescimento de memória é leak?

Repita o ciclo que cria e descarta a tela, capture o heap e verifique se objetos que deveriam morrer continuam alcançáveis por uma GC root. Crescimento temporário, cache limitado e alocações coletáveis não provam leak.

O Network Inspector captura qualquer biblioteca HTTP?

Não conte com isso. A compatibilidade varia por biblioteca, versão e configuração. Se uma chamada não aparece, confirme a documentação da ferramenta e use logging seguro, proxy de teste ou instrumentação apropriada sem expor dados sensíveis.

Profiler substitui teste em aparelho físico?

Não. Emulator é útil para reprodução, mas CPU, memória, GPU, armazenamento, temperatura e rede diferem. Inclua ao menos um celular representativo do público nas validações importantes.

Conclusão

O Android Studio Profiler é mais valioso quando deixa de ser uma tela aberta “para ver se algo parece alto” e vira parte de um método: definir o sintoma, reproduzir, capturar um intervalo curto, formular uma hipótese, alterar pouco e medir novamente.

Use CPU e System Trace para descobrir onde o tempo desaparece, Memory Profiler para entender alocações e retenções, e Network Inspector para enxergar chamadas lentas ou repetidas. Depois, converta correções críticas em benchmarks e monitore o comportamento real do app.

Se você está organizando uma trilha de performance Android, avance para AndroidX App Startup com Kotlin, Baseline Profiles e Macrobenchmark e o guia completo de performance Kotlin. Assim, diagnóstico, otimização e prevenção passam a fazer parte do mesmo fluxo de engenharia.