Tempo de inicialização do app

Os usuários esperam que os apps sejam responsivos e carreguem rapidamente. 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 total (TTFD, na sigla em inglês). O TTID é o tempo necessário para mostrar o primeiro frame, e o TTFD é o tempo que leva para o app se tornar totalmente interativo. Ambos são igualmente importantes, já que o TTID permite que o usuário saiba 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. Inicialize a interface.
  5. Desenhe a interface na tela.

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.

Para mais informações sobre as fases de layout no Compose, consulte Fases do Jetpack Compose.

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

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 vai invocar o método onCreate no objeto do app. Em seguida, o app gera a linha de execução principal (também conhecida como linha de execução de interface) e a encarrega de criar a atividade de host do app.

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. Em um app do Jetpack Compose, a sobrecarga do método onCreate geralmente vem da composição inicial. Isso acontece quando o app chama setContent e invoca os elementos combináveis de nível superior. Uma hierarquia de interface profunda ou complexa ou elementos combináveis que realizam cálculos pesados na linha de execução principal podem aumentar esse tempo de composição.

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 a atividade do host do app em primeiro plano. Se toda a interface do app ainda estiver na memória, ele poderá evitar a repetição da inicialização de objetos, da interface e da renderização.

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 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 1. 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. Caso ela não esteja disponível, tente capturar um rastro usando o app de rastreamento do sistema no dispositivo.

    Figura 2. 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 até a linha com o app em questão. Para abri-la, clique na primeira célula.

  5. Aumente o zoom da linha de execução principal, geralmente localizado 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 3. A 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.

Ao analisar um aplicativo Jetpack Compose, use o rastreamento de composição para ver detalhes granulares sobre o desempenho da sua interface. Preste atenção especial às seções de rastreamento na linha de execução principal que começam com Choreographer#doFrame. Procure intervalos como Compose:recompose, Compose:layout e Compose:draw. Elas mostram quanto tempo o app gasta compondo, medindo, posicionando e desenhando componentes específicos.

Usar métricas para inspecionar e melhorar a inicialização

Para diagnosticar adequadamente a performance 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 de tempo para exibição inicial (TTID) e tempo para exibição total (TTFD) 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 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 demora demais para inicializar.

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 conferir informações 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) é 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 app baixo ajuda a melhorar a experiência do usuário, permitindo que ele inicie o 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, incluindo a sequência de eventos abaixo:

  • Início do processo
  • Inicialização dos objetos
  • Criação e inicialização da atividade do host.
  • Initializing the UI.
  • Exibição do app pela primeira vez

Extrair o TTID

Para encontrar o TTID, pesquise na ferramenta de linha de comando Logcat uma linha de saída com 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 no menu suspenso de filtros e encontre o tempo de Displayed, conforme mostrado na Figura 4. A desativação dos filtros é necessária porque o servidor do sistema, não o app em si, atende a esse registro.

Figura 4. 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 na composição inicial (como dados ou imagens carregados de forma assíncrona) ou recursos 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. Para mais informações sobre recursos, consulte Recursos no Compose.

Às vezes, a linha Displayed na saída do Logcat contém um outro campo para o tempo total, como no exemplo a seguir:

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, geralmente a atividade do host. A medição do tempo total começa no início do processo do app e pode incluir outra atividade que é iniciada primeiro, mas não é exibida na tela. A medição de tempo total só é mostrada quando há uma diferença entre o 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, poderá medir o TTID executando o app com o comando do 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

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) é o tempo que leva para um app se tornar interativo para o usuário. Ele é informado 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. Em geral, esse é o conteúdo principal carregado pela rede ou pelo disco, conforme informado pelo app. Em outras palavras, o TTFD inclui o TTID, bem como o tempo necessário para que o app possa ser usado. Manter o TTFD baixo ajuda a melhorar a experiência do usuário, permitindo uma interação rápida.

Embora o sistema possa determinar o TTID quando a janela do host renderiza o frame inicial, ele não pode determinar automaticamente o TTFD. Como os apps geralmente carregam o conteúdo principal de forma assíncrona, o sistema não sabe quando o app está totalmente utilizável para o usuário. Para determinar o TTFD, o app precisa indicar ao sistema quando atinge o estado de exibição total.

Extrair o TTFD

Para encontrar o TTFD, indique o estado de exibição total chamando o método reportFullyDrawn da ComponentActivity. O método reportFullyDrawn informa quando o app está 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 reportFullyDrawn é chamado. Se você não chamar reportFullyDrawn, nenhum valor de TTFD será informado.

Para medir o TTFD, chame reportFullyDrawn depois de mostrar completamente a interface e todos os dados. Não chame reportFullyDrawn antes que a janela da primeira atividade seja mostrada pela primeira vez, conforme medido pelo sistema, porque ele informa o tempo que o sistema mediu. Em outras palavras, se você chamar reportFullyDrawn antes que o sistema detecte o TTID, ele vai informar que o TTID e o TTFD têm o mesmo valor, e esse valor é o do 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

A saída do Logcat pode incluir um tempo total, conforme discutido em Tempo para exibição inicial.

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

Você pode usar reportFullyDrawn para indicar o estado de exibição total em casos básicos, quando você sabe que o estado de exibição total foi alcançado. No entanto, nos casos em que as linhas de execução em segundo plano precisam concluir o trabalho antes que o estado de exibição total seja alcançado, é necessário atrasar reportFullyDrawn para uma medição de TTFD mais precisa. Para aprender a atrasar reportFullyDrawn, consulte a seção abaixo.

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, como quando o app estiver buscando imagens da rede, atrase a chamada de reportFullyDrawn até que o app se torne usável. Assim, você pode incluir o preenchimento da lista como parte do tempo de comparação.

Por exemplo, se a interface contém uma lista dinâmica, como LazyColumn ou LazyRow, talvez ela seja preenchida por uma tarefa em segundo plano concluída depois que a lista for renderizada e, portanto, depois que a interface estiver 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, extraia o FullyDrawnReporter usando fullyDrawnReporter e adicione um informante no código do app. Libere o informante depois que a tarefa em segundo plano terminar de preencher a lista.

O FullyDrawnReporter não vai chamar o método reportFullyDrawn até que todos os informantes adicionados sejam liberados. Ao adicionar um informante, mas não liberá-lo até que o processo em segundo plano seja concluído, você garante que os dados de marcação de tempo de inicialização incluam o tempo de preenchimento da lista, sem mudar o comportamento do app para o usuário. O reportFullyDrawn não é chamado até que todas as tarefas sejam concluídas, independente da ordem.

Se o app usa o Jetpack Compose, você pode usar as APIs abaixo para indicar o estado de exibição total:

  • ReportDrawn: indica que o elemento combinável está pronto para interação.
  • ReportDrawnWhen: usa um predicado, como 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.

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

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 {
                fullyDrawnReporter.addReporter()
                lifecycleScope.launch(Dispatchers.IO) {
                    // Perform the background operation.
                    fullyDrawnReporter.removeReporter()
                }
                fullyDrawnReporter.addReporter()
                lifecycleScope.launch(Dispatchers.IO) {
                    // Perform the background operation.
                    fullyDrawnReporter.removeReporter()
                }
            }
        }
    }
}
Identificar gargalos

Para procurar gargalos, você pode usar o CPU Profiler do Android Studio. Para saber mais, consulte Inspecionar atividades de CPU com o CPU Profiler.

Você também pode conferir mais detalhes 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ências como 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:

  • Inicializar uma interface grande ou complexa.
  • Inicialização pesada em elementos combináveis.
  • Bloquear desenho de tela em disco ou E/S de rede.
  • Carregar e decodificar bitmaps.
  • Como fazer varredura de objetos VectorDrawable.
  • Inicialização de outros subsistemas da atividade do host do app.

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 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, 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 interface, mais tempo o app levará para inicializá-la. Considere as seguintes etapas ao resolver esse problema:
    • Nivele sua hierarquia de UI reduzindo elementos combináveis redundantes ou aninhados.
    • Evite composição e recomposição desnecessárias durante o lançamento.
    • Adie a composição da interface não essencial.
  • 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 desta forma:
    • Mova toda a inicialização de recursos para outra linha de execução para que o app possa inicializá-los lentamente.
    • Carregue e mostre a interface com dados de marcador de posição primeiro e, depois, atualize as propriedades visuais que dependem de bitmaps e outros recursos.

Para mais informações sobre recomposição, consulte Recomposição e Receber contagens de recomposição.

Telas de apresentação personalizadas

Talvez você note 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 (nível 30 da API) ou versões anteriores:

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

No Android 12 e versões mais recentes, é necessário migrar para a API SplashScreen. Ela possibilita um tempo de inicialização mais rápido e também um ajuste de tela de apresentação destas formas:

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 mostrar a tela de apresentação em todas as versões do Android.

Para saber mais, consulte o guia para migrar a tela de apresentação.