Tempo de inicialização do app

Os usuários esperam que os apps sejam carregados rapidamente e sejam responsivos. Um app com tempo de inicialização lento não atende a essa expectativa e pode decepcionar 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.

Esta página fornece informações para otimizar o tempo de inicialização do seu app, incluindo uma visão geral das partes internas do processo de lançamento, como criar um perfil do desempenho de inicialização, alguns problemas comuns e dicas para resolvê-los.

Entender os diferentes estados de inicialização do app

A inicialização do app pode ocorrer em um destes três estados: inicialização a frio, inicialização com estado salvo ou inicialização a quente. Cada estado 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.

Duas métricas importantes para determinar a inicialização do app são o tempo para exibição inicial (TTID, na sigla em inglês) e o tempo para exibição completa (TTFD, na sigla em inglês). O TTID é o tempo necessário para mostrar o primeiro frame, e TTFD é o tempo que leva para o app se tornar totalmente interativo. Ambos são igualmente importantes, porque o TTID informa ao usuário que o app está sendo carregado e o TTFD informa quando o app pode ser usado. Se um deles for muito longo, o usuário poderá sair do app antes mesmo de ele ser totalmente carregado.

Inicialização a frio

Uma inicialização a frio refere-se a um app que é inicializado do zero. Isso significa que, até esse início, o processo do sistema cria o processo do app. Inicializações a frio ocorrem 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 representa o maior desafio para 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 estas três tarefas:

  1. Carregar e iniciar o app.
  2. Mostrar uma janela de inicialização em branco para o app imediatamente após o início.
  3. Criar o processo do app.

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

  1. Criar o objeto de app.
  2. Iniciar a linha de execução principal.
  3. Criar a atividade principal.
  4. Inflar visualizações.
  5. Fazer o layout da tela.
  6. Executar o desenho inicial.

Quando o processo do app conclui o primeiro desenho, o processo do sistema substitui a janela de segundo plano exibida 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 a criação da atividade.

Criação de apps

Quando o app é iniciado, a janela inicial em branco permanece na tela até o sistema concluir o desenho do app pela primeira vez. Nesse momento, o processo do sistema troca a janela inicial do app, permitindo que o usuário interaja com ele.

Se você substituir Application.onCreate() no seu próprio app, o sistema invocará o método onCreate() no objeto do app. Depois, o app vai gerar a linha de execução principal, também conhecida como linha de execução de interface, 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 a maior sobrecarga: carregando e inflando visualizações e inicializando os objetos necessários para a execução da atividade.

Inicialização com estado salvo

Uma inicialização com estado salvo abrange um subconjunto das operações que ocorrem 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ções com estado salvo, como:

  • O usuário sai do app e depois o reinicializa. O processo pode continuar em execução, mas o app precisa recriar a atividade do zero usando 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 tem uma sobrecarga menor do que uma inicialização a frio. Em uma inicialização a quente, o sistema coloca sua atividade em primeiro plano. Se todas as atividades do app ainda estiverem na memória, ele poderá evitar a repetição da inicialização, a inflação de layouts e a renderização de objetos.

No entanto, se alguma memória for 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. Um diagrama com os vários estados de inicialização e os respectivos processos. Cada estado começa no primeiro frame desenhado.

Como identificar a inicialização do app no Perfetto

Para depurar problemas de inicialização do app, determine exatamente o que está incluído na fase de inicialização. Para identificar toda a fase de inicialização do app no Perfetto, siga estas etapas:

  1. No Perfetto, encontre a linha com a métrica derivada para inicialização de apps Android. Se ele não estiver disponível, tente capturar um rastro usando o app de rastreamento do sistema no dispositivo.

    Figura 3. Fração da métrica derivada para inicialização de apps Android no Perfetto.
  2. Clique na fração associada e pressione m para selecioná-la. Os colchetes aparecem ao redor da fração e indicam o tempo transcorrido. A duração também é mostrada na guia Seleção atual.

  3. Fixe a linha "Android App Startups" clicando no ícone de fixação, que fica visível ao manter o ponteiro sobre a linha.

  4. Role a tela até a linha com o app em questão e clique na primeira célula para expandi-la.

  5. Aumente o zoom da linha de execução principal, geralmente na parte de cima, pressionando w. Pressione s, a, d para diminuir o zoom, mover para a esquerda e mover para a direita, respectivamente.

    Figura 4. Fração da métrica derivada para inicialização de apps Android ao lado da linha de execução principal do app.
  6. A fração da métrica derivada facilita a visualização exata do que está incluído na inicialização do app para que você possa continuar a depuração com mais detalhes.

Use métricas para inspecionar e melhorar as startups

Para diagnosticar adequadamente o desempenho do tempo de inicialização, é possível rastrear métricas que mostram o tempo necessário para que o app seja iniciado. O Android oferece várias maneiras de mostrar que o app está com um problema e ajuda a diagnosticá-lo. O recurso "Android vitals" pode alertar que um 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 (TTID, na sigla em inglês) e tempo para exibição total (TTFD, na sigla em inglês) 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 com eficiência o código e 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 inicialização.

O Android vitals considera os seguintes tempos de inicialização do app excessivos:

  • 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 (TTID). Para saber mais sobre como o Google Play coleta dados do Android vitals, consulte a documentação do Play Console.

Tempo para exibição inicial

O tempo para exibição inicial (TTID, na sigla em inglês) é o tempo necessário para mostrar o primeiro frame da interface do app. Essa métrica mede o tempo que um app leva para produzir o primeiro frame, incluindo a inicialização do processo durante uma inicialização a frio, a criação de atividades durante uma inicialização a frio ou com estado salvo e a exibição do primeiro frame. Manter o TTID do seu app baixo ajuda a melhorar a experiência do usuário, permitindo que ele veja a inicialização do app rapidamente. O TTID é informado automaticamente para cada app pelo framework do Android. Ao otimizar a inicialização do app, recomendamos implementar reportFullyDrawn para receber informações até o TTFD.

O TTID é medido como um valor de tempo que representa o tempo total decorrido que inclui a seguinte sequência de eventos:

  • Iniciando o processo.
  • Inicializar os objetos.
  • Criar e inicializar a atividade.
  • Inflar o layout.
  • Como desenhar o app pela primeira vez.

Recuperar TTID

Para encontrar o TTID, pesquise na ferramenta de linha de comando Logcat por uma linha de saída que contenha um valor chamado Displayed. Esse valor é o TTID e é semelhante ao exemplo abaixo, em que o TTID é de 3s534ms:

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

Para encontrar o TTID no Android Studio, desative os filtros na visualização do Logcat na lista suspensa de filtros e encontre o tempo de Displayed, conforme mostrado na Figura 5. A desativação dos filtros é necessária porque o servidor do sistema, não o app em si, atende a esse registro.

Figura 5. Filtros desativados e o valor Displayed no Logcat.

A métrica Displayed na saída do Logcat não captura necessariamente o tempo até que todos os recursos sejam carregados e mostrados. Ela exclui 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 campo extra para o tempo total. Por exemplo:

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

Nesse caso, a primeira medição de tempo é apenas da atividade que é 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 é iniciada primeiro, mas que não é 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.

Recomendamos o uso do Logcat no Android Studio. No entanto, se você não estiver usando o Android Studio, também poderá medir o TTID executando o app com o comando do gerenciador de atividades do shell 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

A métrica Displayed aparece na saída do Logcat como antes. A janela de terminal mostra 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

O tempo para exibição total (TTFD, na sigla em inglês) é o tempo que um app leva para se tornar interativo para o usuário. Ela é informada como o tempo necessário para mostrar o primeiro frame da interface do app, bem como o conteúdo que é carregado de forma assíncrona após a exibição do frame inicial. Geralmente, esse é o conteúdo principal carregado pela rede ou pelo disco, conforme relatado pelo app. Em outras palavras, o TTFD inclui o TTFD, bem como o tempo necessário para que o app seja utilizável. Manter o TTFD baixo ajuda a melhorar a experiência do usuário, permitindo que os usuários interajam com o app rapidamente.

O sistema determina o TTID quando Choreographer chama o método onDraw() da atividade e quando sabe que o chama pela primeira vez. No entanto, o sistema não sabe quando determinar o TTFD, porque cada app se comporta de maneira diferente. Para determinar o TTFD, o app precisa sinalizar ao sistema quando atinge o estado totalmente renderizado.

Recuperar o TTFD

Para encontrar o TTFD, sinalize o estado totalmente renderizado chamando o método reportFullyDrawn() da ComponentActivity. O método reportFullyDrawn informa quando o app é totalmente renderizado e em um estado utilizável. O TTFD é o tempo decorrido entre o momento em que o sistema recebe a intent de inicialização do app e o momento em que o reportFullyDrawn() é chamado. Se você não chamar reportFullyDrawn(), nenhum valor de TTFD será informado.

Para medir o TTFD, chame reportFullyDrawn() depois de desenhar completamente a interface e todos os dados. Não chame reportFullyDrawn() antes que a janela da primeira atividade seja renderizada e mostrada conforme medido pelo sistema, porque, assim, ele informa que o sistema mede o tempo. Em outras palavras, se você chamar reportFullyDrawn() antes de o sistema detectar o TTID, ele vai informar o TTID e o TTFD como o mesmo valor, e esse valor será o de TTID.

Quando você usa reportFullyDrawn(), o Logcat mostra uma saída como o exemplo abaixo, em que o TTFD é de 1s54ms:

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 os tempos de exibição forem mais lentos do que o esperado, tente identificar os gargalos no processo de inicialização.

Você pode usar reportFullyDrawn() para sinalizar o estado totalmente renderizado em casos básicos, em que você sabe que o estado totalmente renderizado foi alcançado. No entanto, nos casos em que as linhas de execução em segundo plano precisam concluir o trabalho em segundo plano antes que o estado totalmente renderizado seja alcançado, é necessário atrasar reportFullyDrawn() para uma medição de TTFD mais precisa. Para saber como atrasar reportFullyDrawn(), consulte a seção a seguir.

Melhorar a precisão da marcação do tempo de inicialização

Se o app estiver executando o carregamento lento e a tela inicial não incluir todos os recursos, por exemplo, quando o app estiver buscando imagens da rede, atrase a chamada de reportFullyDrawn até que o app se torne usável para que você possa incluir o preenchimento da lista como parte do tempo de comparação.

Por exemplo, se a interface tiver uma lista dinâmica, como uma RecyclerView ou lista lenta, ela poderá ser preenchida por uma tarefa em segundo plano concluída após a lista ser desenhada pela primeira vez e, portanto, depois que a interface for marcada como totalmente renderizada. Nesses casos, o preenchimento da lista não é incluído na comparação.

Para incluir o preenchimento da lista como parte da marcação do tempo de comparação, acesse o FullyDrawnReporter usando getFullyDrawnReporter() e adicione um informador a ele no código do app. Libere o informante depois que a tarefa em segundo plano terminar de preencher a lista.

FullyDrawnReporter não chama o método reportFullyDrawn() até que todos os informantes adicionados sejam liberados. Ao adicionar um informante até que o processo em segundo plano seja concluído, as marcações de tempo também vão incluir a quantidade de tempo necessária para preencher a lista nos dados de marcação de tempo de inicialização. Isso não muda o comportamento do app para o usuário, mas permite que os dados da marcação de tempo incluam o tempo necessário para preencher a lista. reportFullyDrawn() não é chamado até que todas as tarefas sejam concluídas, independentemente da ordem.

O exemplo abaixo mostra como é possível executar várias tarefas em segundo plano simultaneamente, cada uma registrando o próprio informante:

Kotlin

class MainActivity : ComponentActivity() {

    sealed interface ActivityState {
        data object LOADING : ActivityState
        data object LOADED : ActivityState
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            var activityState by remember {
                mutableStateOf(ActivityState.LOADING as ActivityState)
            }
            fullyDrawnReporter.addOnReportDrawnListener {
                activityState = ActivityState.LOADED
            }
            ReportFullyDrawnTheme {
                when(activityState) {
                    is ActivityState.LOADING -> {
                        // Display the loading UI.
                    }
                    is ActivityState.LOADED -> {
                        // Display the full UI.
                    }
                }
            }
            SideEffect {
                lifecycleScope.launch(Dispatchers.IO) {
                    fullyDrawnReporter.addReporter()

                    // Perform the background operation.

                    fullyDrawnReporter.removeReporter()
                }
                lifecycleScope.launch(Dispatchers.IO) {
                    fullyDrawnReporter.addReporter()

                    // Perform the background operation.

                    fullyDrawnReporter.removeReporter()
                }
            }
        }
    }
}

Java

public class MainActivity extends ComponentActivity {
    private FullyDrawnReporter fullyDrawnReporter;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        fullyDrawnReporter = getFullyDrawnReporter();
        fullyDrawnReporter.addOnReportDrawnListener(() -> {
            // Trigger the UI update.
            return Unit.INSTANCE;
        });

        new Thread(new Runnable() {
            @Override
            public void run() {
                fullyDrawnReporter.addReporter();

                // Do the background work.

               fullyDrawnReporter.removeReporter();
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                fullyDrawnReporter.addReporter();

                // Do the background work.

                fullyDrawnReporter.removeReporter();
            }
        }).start();
    }
}

Se o app usa o Jetpack Compose, é possível usar as APIs abaixo para indicar o estado totalmente renderizado:

  • ReportDrawn: indica que o elemento combinável está pronto para interação.
  • ReportDrawnWhen: usa um predicado, por exemplo, list.count > 0, para indicar quando o elemento combinável está pronto para interação.
  • ReportDrawnAfter: usa um método de suspensão que, quando concluído, indica que o elemento combinável está pronto para interação.
Identificar gargalos

Para procurar gargalos, você pode usar 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 saber mais sobre o rastreamento inline, consulte a documentação das funções Trace e a visão geral do rastreamento do sistema.

Resolver 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 Application realizarem inicializações que ainda não precisam ser feitas.

Algumas inicializações podem ser completamente desnecessárias, como ao inicializar informações de estado para a atividade principal quando o app é realmente 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 Android Runtime (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, continue investigando para descobrir qual trabalho está ocorrendo.

Rastreamento in-line

Use o rastreamento inline para investigar as causas prováveis, incluindo as seguintes:

  • 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, inicialize apenas os objetos 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ência como o Hilt, que cria objetos e dependências quando injetados pela primeira vez.

Se o app usa provedores de conteúdo para iniciar 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. Esses problemas comuns incluem o seguinte:

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

Diagnosticar o problema

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

Rastreamento de métodos

Ao usar o CPU Profiler, preste atenção aos construtores da 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, continue investigando para descobrir qual trabalho está ocorrendo.

Rastreamento in-line

Use o rastreamento inline para investigar as causas prováveis, incluindo as seguintes:

  • A função onCreate() inicial do 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 seguir duas etapas para resolver esse problema:
    • Nivelar sua hierarquia de visualizações, reduzindo layouts redundantes ou aninhados.
    • Não infle partes da interface 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 que o app possa executá-la lentamente em uma linha de execução diferente.
    • Deixe o app carregar e mostrar as visualizações e, em seguida, 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:

  • Uso do atributo de tema windowDisablePreview para desativar a tela em branco inicial desenhada pelo sistema durante a inicialização.
  • Uso de um Activity dedicado.

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 de 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 Migração da tela de apresentação.