Tempo de inicialização do app

Os usuários esperam que os apps sejam responsivos e rápidos para carregar. Um app com tempo de inicialização lento não atende a essa expectativa e pode ser decepcionante para os usuários. Esse tipo de experiência ruim pode fazer com que o usuário dê uma classificação negativa ao seu app na Play Store ou até mesmo desista de usá-lo.

Este documento fornece informações para ajudar a otimizar o tempo de inicialização do seu app. Ele começa explicando os aspectos internos do processo de inicialização. Em seguida, ele mostra como analisar o desempenho da inicialização. Por fim, descreve alguns problemas comuns no tempo de inicialização e dá algumas dicas sobre como resolvê-los.

Entender os diferentes estados de inicialização do app

A inicialização de um app pode ocorrer em um de três estados: inicialização a frio, inicialização com estado salvo ou inicialização a quente. Cada um desses estados afeta o tempo que leva para o app ficar visível para o usuário. Em uma inicialização a frio, o app é iniciado do zero. Nos outros estados, o sistema precisa levar o app que está em execução em segundo plano para o primeiro plano. Recomendamos que você sempre otimize presumindo que se trate de uma inicialização a frio. Ao fazer isso, o desempenho de inicializações a quente e com estado salvo também pode melhorar.

Para otimizar seu app para uma inicialização rápida, vale a pena entender o que está acontecendo nos níveis do sistema e do app e como eles interagem em cada um desses estados.

Inicialização a frio

Uma inicialização a frio refere-se a um app que é inicializado do zero: o processo do sistema, até a inicialização, não criou o processo do app. As inicializações a frio acontecem em casos em que o app é iniciado pela primeira vez desde que o dispositivo foi inicializado ou quando o sistema o eliminou. Esse tipo de inicialização apresenta o maior desafio em termos de minimizar o tempo de inicialização, porque o sistema e o app têm mais trabalho a fazer do que nos outros estados de inicialização.

No início de uma inicialização a frio, o sistema tem três tarefas. São elas:

  1. Carregamento e inicialização do app.
  2. Exibição de uma janela de inicialização em branco para o app imediatamente após o início.
  3. Crie o processo do app.

Assim que é criado pelo sistema, o processo do app fica responsável pelas próximas etapas:

  1. Criação do objeto do app.
  2. Inicialização da linha de execução principal.
  3. Criação da atividade principal.
  4. Aumento das visualizações.
  5. Definição do layout da tela.
  6. Execução do desenho inicial.

Depois que o processo do app conclui o primeiro desenho, o processo do sistema substitui a janela de segundo plano exibida no momento pela atividade principal. Nesse ponto, o usuário pode começar a usar o app.

A figura 1 mostra como os processos do sistema e do app transferem o trabalho entre si.

Figura 1. Uma representação visual das partes importantes da inicialização a frio de um app.

Problemas de desempenho podem surgir durante a criação do app e da atividade.

Criação do app

Quando o aplicativo é iniciado, a janela inicial em branco permanece na tela até o sistema concluir o desenho do app pela primeira vez. Nesse ponto, o processo do sistema substitui a janela inicial do app, permitindo que o usuário comece a interagir com o app.

Se você substituiu Application.onCreate() no seu próprio app, o sistema invoca o método onCreate() no objeto do app. Depois, o app gera a linha de execução principal (também conhecida como linha de execução da UI) e a encarrega de criar sua atividade principal.

A partir desse ponto, os processos no nível do sistema e do app prosseguem de acordo com os estágios do ciclo de vida do app.

Criação da atividade

Depois que o processo do app cria sua atividade, ela executa as seguintes operações:

  1. Inicialização de valores.
  2. Chamada de construtores.
  3. Chama o método de callback, por exemplo, Activity.onCreate(), apropriado para o estado atual do ciclo de vida da atividade.

Normalmente, o método onCreate() tem o maior impacto no tempo de carregamento, porque executa o trabalho com maior overhead: carrega e infla visualizações e inicializa os objetos necessários para a execução da atividade.

Inicialização com estado salvo

Uma inicialização com estado salvo inclui um subconjunto das operações que acontecem durante uma inicialização a frio. Ao mesmo tempo, ela representa mais sobrecarga do que uma inicialização a quente. Há muitos estados em potencial que podem ser considerados inicialização com estado salvo. Por exemplo:

  • O usuário sai do app e depois o reinicializa. O processo pode ter continuado a ser executado, mas o app precisa recriar a atividade do zero fazendo uma chamada para onCreate().

  • O sistema elimina seu app da memória e, em seguida, o usuário o reinicializa. O processo e a atividade precisam ser reiniciados, mas a tarefa pode se beneficiar um pouco do pacote de estado da instância salvo, transmitido para onCreate().

Inicialização a quente

Uma inicialização a quente do app é muito mais simples e com menos sobrecarga que uma inicialização a frio. Nesse tipo de inicialização, o sistema simplesmente leva sua atividade para o primeiro plano. Se todas as atividades do seu app ainda estiverem na memória, o app poderá evitar a repetição da inicialização, a inflação do layout e renderização de objetos.

No entanto, se alguma memória tiver sido limpa em resposta a eventos de corte de memória, como onTrimMemory(), esses objetos precisarão ser recriados em resposta ao evento de inicialização a quente.

Uma inicialização a quente exibe o mesmo comportamento na tela que o do cenário de uma inicialização a frio:

O processo do sistema mostra uma tela em branco até que o app termine de renderizar a atividade.

Figura 2. Este diagrama mostra os vários estados de inicialização e os respectivos processos. Cada estado começa no primeiro frame renderizado.

Usar métricas para detectar e diagnosticar problemas

Para diagnosticar adequadamente o desempenho dos tempos de inicialização, você pode rastrear métricas que mostram o tempo necessário para que o app seja iniciado. O Android oferece vários meios de informar que seu app está com um problema e ajudar a diagnosticá-lo. O recurso "Android vitals" pode alertar que o problema está ocorrendo, e as ferramentas de diagnóstico podem ajudar a diagnosticar o problema.

Benefícios do uso de métricas de inicialização

O Android usa as métricas Tempo para exibição inicial e Tempo para exibição total para otimizar as inicializações de app a frio e com estado salvo. O Android Runtime (ART) usa os dados dessas métricas para pré-compilar de forma eficiente o código e assim otimizar futuras inicializações.

Inicializações mais rápidas levam a uma interação mais consistente com o app, o que reduz as instâncias de saída antecipada, a reinicialização da instância ou a saída para outro app.

Android vitals

O Android vitals pode ajudar a melhorar o desempenho do seu app, mostrando alertas no Play Console quando o app exceder os tempos de execução. O Android vitals considera os tempos de inicialização do app excessivos quando:

  • a inicialização a frio do app leva 5 segundos ou mais;
  • a inicialização com estado salvo leva 2 segundos ou mais;
  • a inicialização a quente leva 1,5 segundo ou mais.

O Android vitals usa a métrica Tempo para exibição inicial. Para ver informações sobre como o Google Play coleta dados do Android vitals, consulte a documentação do Play Console.

Tempo para exibição inicial

A métrica de tempo para exibição inicial (TTID, na sigla em inglês) mede o tempo que um aplicativo leva para produzir o primeiro frame, incluindo a inicialização do processo (se for uma inicialização a frio), a criação de atividades (se for a frio/com estado salvo) e a exibição do primeiro frame.

Como recuperar o TTID

No Android 4.4 (API de nível 19) ou versões mais recentes, o logcat inclui uma linha de saída que contém um valor chamado Displayed. Esse valor representa o tempo total decorrido entre o início do processo e o término do desenho da atividade correspondente na tela. O tempo decorrido abrange a seguinte sequência de eventos:

  • Início do processo.
  • Inicialização dos objetos.
  • Criação e inicialização da atividade.
  • Inflação do layout.
  • Desenho do app pela primeira vez.

A linha de registro informada é semelhante a este exemplo:

ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms

Se você estiver rastreando a saída do logcat na linha de comando ou em um terminal, será fácil encontrar o tempo decorrido. Para encontrá-lo no Android Studio, é preciso desativar os filtros na visualização do logcat. A desativação dos filtros é necessária porque o servidor do sistema, não o app em si, atende a esse registro.

Depois de definir as configurações adequadas, você pode pesquisar facilmente o termo correto para conferir o tempo. A Figura 2 mostra como desativar filtros e, na segunda linha de saída da parte de baixo, um exemplo de saída do Logcat do tempo Displayed.

Figura 2. Como desativar filtros e encontrar o valor "Displayed" no logcat.

A métrica Displayed na saída do Logcat não captura necessariamente o tempo total até que todos os recursos sejam carregados e mostrados. Ela exclui os recursos que não são referenciados no arquivo de layout ou que o app cria como parte da inicialização do objeto. Ela exclui esses recursos, porque o carregamento deles é um processo inline e não bloqueia a exibição inicial do app.

Às vezes, a linha Displayed na saída do logcat contém um outro campo para o tempo total. Exemplo:

ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms (total +1m22s643ms)

Nesse caso, a primeira medição de tempo é apenas da atividade que foi desenhada pela primeira vez. A medição do tempo total começa no início do processo do app e pode incluir outra atividade que foi iniciada primeiro, mas não foi exibida na tela. A medição de tempo total só é exibida quando há uma diferença entre tempo de inicialização da atividade individual e o total.

Você também pode medir o tempo para a exibição inicial executando seu app com o comando de Gerenciador de atividades do shell do adb. Veja um exemplo:

adb [-d|-e|-s <serialNumber>] shell am start -S -W
com.example.app/.MainActivity
-c android.intent.category.LAUNCHER
-a android.intent.action.MAIN</pre>

A métrica Displayed aparece na saída do logcat como antes. A janela do terminal também exibe o seguinte:

Starting: Intent
Activity: com.example.app/.MainActivity
ThisTime: 2044
TotalTime: 2044
WaitTime: 2054
Complete

Os argumentos -c e -a são opcionais e permitem especificar <category> e <action>.

Tempo para exibição total

A métrica "Tempo para exibição total" (TTFD, na sigla em inglês) mede o tempo que o aplicativo leva para produzir o primeiro frame com conteúdo completo, incluindo o conteúdo carregado de forma assíncrona após o primeiro frame. Em geral, esse é o conteúdo da lista principal carregado pela rede, conforme relatado pelo app.

Como recuperar o TTFD

Você pode usar o método reportFullyDrawn() para medir o tempo decorrido entre a inicialização do app e a exibição completa de todos os recursos e das hierarquias de visualização. Isso pode ser valioso nos casos em que um app executa carregamento lento. No carregamento lento, um app não bloqueia o desenho inicial da janela, mas carrega recursos de forma assíncrona e atualiza a hierarquia de visualização.

Se, devido ao carregamento lento, a exibição inicial de um app não incluir todos os recursos, considere o carregamento e a exibição completos de todos os recursos e visualizações como uma métrica separada. Por exemplo, sua IU pode estar totalmente carregada, com algum texto desenhado, mas ainda não exibe imagens que o app precisa buscar na rede.

Para resolver esse problema, chame reportFullyDrawn() manualmente para informar ao sistema que sua atividade foi concluída com o carregamento lento. Quando você usa esse método, o valor exibido pelo logcat é o tempo decorrido entre a criação do objeto do app e o momento em que reportFullyDrawn() é chamado. Veja um exemplo da saída do logcat:

system_process I/ActivityManager: Fully drawn {package}/.MainActivity: +1s54ms

Às vezes, a saída do logcat inclui um tempo total, conforme discutido em Tempo para exibição inicial.

Se você descobrir que os tempos de exibição estão mais lentos do que o esperado, tente identificar os gargalos no processo de inicialização.

Identificar gargalos

Uma boa maneira de procurar gargalos é usando o CPU Profiler do Android Studio. Para mais informações, consulte Inspecionar atividades de CPU com o CPU Profiler.

Você também pode saber mais sobre possíveis gargalos usando o rastreamento inline nos métodos onCreate() dos seus apps e atividades. Para mais informações sobre o rastreamento inline, consulte a documentação das funções Trace e a visão geral do rastreamento do sistema.

Conhecer problemas comuns

Esta seção discute vários problemas que geralmente afetam o desempenho da inicialização dos apps. Esses problemas referem-se principalmente à inicialização de apps e objetos de atividade, bem como ao carregamento de telas.

Inicialização de apps pesados

O desempenho de inicialização pode ser afetado quando seu código modifica o objeto Application e executa um trabalho pesado ou lógica complexa ao inicializar esse objeto. Seu app poderá perder tempo durante a inicialização se as subclasses de Application executarem inicializações que não precisam ser realizadas. Algumas inicializações podem ser completamente desnecessárias. Por exemplo, inicializar informações sobre o estado da atividade principal quando o app foi iniciado em resposta a uma intent. Com uma intent, o app usa apenas um subconjunto dos dados de estado inicializados anteriormente.

Outros desafios durante a inicialização do app incluem eventos de coleta de lixo impactantes ou numerosos, ou E/S de disco que aconteçam simultaneamente com a inicialização, bloqueando ainda mais o processo de inicialização. A coleta de lixo é especialmente uma consideração no tempo de execução do Dalvik. O tempo de execução de ART executa a coleta de lixo simultaneamente, minimizando o impacto dessa operação.

Diagnosticar o problema

Você pode usar o rastreamento de métodos ou in-line para tentar diagnosticar o problema.

Rastreamento de métodos

A execução do CPU Profiler revela que o método callApplicationOnCreate() finalmente chama seu método com.example.customApplication.onCreate. Se a ferramenta mostrar que esses métodos estão demorando muito para terminar a execução, você precisará explorar mais para ver qual trabalho está ocorrendo.

Rastreamento in-line

Use o rastreamento in-line para investigar causas prováveis, incluindo:

  • A função onCreate() inicial do app.
  • Quaisquer objetos singleton que seu app inicializa.
  • E/S de disco, desserialização ou loop apertado que possa estar ocorrendo durante o gargalo.

Soluções para o problema

Se o problema está nas inicializações desnecessárias ou na E/S de disco, a solução é a inicialização lenta. Em outras palavras, só é necessário inicializar objetos que sejam imediatamente necessários. Em vez de criar objetos estáticos globais, mude para um padrão Singleton, em que o app inicializa objetos apenas na primeira vez que eles são necessários.

Além disso, você pode usar um framework de injeção de dependências como Hilt, que cria objetos e dependências quando injetados pela primeira vez.

Se o app usa provedores de conteúdo para inicializar componentes de apps na inicialização, considere usar a biblioteca App Startup.

Inicialização de atividades pesadas

A criação de atividades geralmente envolve muito trabalho com sobrecarga. Muitas vezes, há oportunidades de otimizar esse trabalho para melhorar o desempenho. Tais problemas comuns incluem:

  • Inflar layouts grandes ou complexos.
  • Bloquear desenho de tela em disco ou E/S de rede.
  • Carregar e decodificar bitmaps.
  • Fazer varredura de objetos VectorDrawable.
  • Inicializar outros subsistemas da atividade.

Diagnosticar o problema

Nesse caso também, o rastreamento de métodos e in-line podem ser úteis.

Rastreamento de métodos

Ao usar o CPU Profiler, preste atenção aos construtores de subclasse Application e aos métodos com.example.customApplication.onCreate().

Se a ferramenta mostrar que esses métodos estão demorando muito para terminar a execução, você precisará explorar mais para ver qual trabalho está ocorrendo.

Rastreamento in-line

Use o rastreamento in-line para investigar causas prováveis, incluindo:

  • A função onCreate() inicial do seu app.
  • Qualquer objeto singleton global inicializado.
  • E/S de disco, desserialização ou loop apertado que possa estar ocorrendo durante o gargalo.

Soluções para o problema

Há muitos gargalos em potencial, mas dois problemas e soluções comuns são os seguintes:

  • Quanto maior for sua hierarquia de visualizações, mais tempo o app levará para inflá-la. Você pode executar duas etapas para solucionar esse problema:
    • Nivelar sua hierarquia de visualizações, reduzindo layouts redundantes ou aninhados.
    • Não inflar partes da IU que não precisam estar visíveis durante a inicialização. Em vez disso, use um objeto ViewStub como um marcador para sub-hierarquias que o app pode inflar em um momento mais apropriado.
  • Colocar toda a inicialização de recursos na linha de execução principal também pode deixar a inicialização lenta. Você pode solucionar esse problema da seguinte maneira:
    • Mova toda a inicialização de recursos para outra linha de execução, para que o app possa inicializá-los lentamente.
    • Permita que o app carregue e mostre as visualizações e só depois atualize as propriedades visuais que dependem de bitmaps e outros recursos.

Telas de apresentação personalizadas

Talvez você veja um tempo extra durante a inicialização se tiver usado um dos seguintes métodos para implementar uma tela de apresentação personalizada no Android 11 (API de nível 30) ou versões anteriores:

  • O atributo de tema windowDisablePreview para desativar a tela em branco inicial desenhada pelo sistema durante a inicialização
  • Uma atividade dedicada

A partir do Android 12, é necessário migrar para a API SplashScreen. Essa API possibilita um tempo de inicialização mais rápido e também um ajuste na tela de apresentação das seguintes maneiras:

Além disso, a biblioteca de compatibilidade faz o backport da API SplashScreen para ativar a compatibilidade com versões anteriores e criar uma aparência consistente para exibição da tela de apresentação em todas as versões do Android.

Para mais detalhes, consulte o guia Migrar a implementação de tela de apresentação.