Tiempo de inicio de la app

Los usuarios esperan que las apps sean responsivas y se carguen con rapidez. Una app con inicio lento no cumple con esta expectativa y puede decepcionar a los usuarios. Este tipo de experiencia negativa puede hacer que un usuario califique tu app de manera deficiente en Play Store o incluso que deje de usarla por completo.

En este documento, se proporciona información que te ayudará a optimizar el tiempo de inicio de tu app. En primer lugar, se explican los aspectos internos del proceso de inicio. Luego, se analiza cómo perfilar el rendimiento del inicio. Por último, se describen algunos problemas comunes de inicio y se ofrecen algunas sugerencias sobre cómo solucionarlos.

Información sobre los diferentes estados de inicio de la app

El inicio de una app se puede llevar a cabo en uno de los tres estados siguientes, que determinan el tiempo en que la app tarda en volverse visible para el usuario: inicio en frío, inicio semicaliente o inicio en caliente. En inicio en frío, tu app se inicia desde cero. En los otros estados, el sistema necesita llevar la ejecución de la app del segundo plano al primero. Te recomendamos que siempre optimices tu app en función de la perspectiva de un inicio en frío, ya que esto también puede mejorar el rendimiento de los inicios semicalientes y en caliente.

A fin de optimizar tu app para un inicio rápido, es útil conocer qué sucede en los niveles del sistema y de la app, y cómo estos interactúan, en cada uno de los estados.

Inicio en frío

El inicio en frío hace referencia a una app que se inicia desde cero: hasta este inicio, el proceso del sistema aún no creó el proceso de la app. Los inicios en frío se producen, por ejemplo, cuando se inicia tu app por primera vez desde que se inició el dispositivo, o desde que el sistema finalizó la actividad de la app. Este tipo de inicio presenta el mayor desafío en términos de minimización del tiempo de inicio, ya que el sistema y la app deben trabajar más que en los otros estados de inicio.

Cuando comienza el inicio en frío, el sistema tiene las tres tareas siguientes:

  1. Cargar e iniciar de la app.
  2. Mostrar una ventana de inicio en blanco para la app inmediatamente después del inicio
  3. Crear el proceso de la app

No bien el sistema crea el proceso de la app, este es responsable de las siguientes etapas:

  1. Crear el objeto de la app.
  2. Iniciar el subproceso principal.
  3. Crear la actividad principal.
  4. Aumentar las vistas.
  5. Diseñar la pantalla.
  6. Realizar la apertura inicial

Una vez que el proceso de la app completó la apertura inicial, el proceso del sistema intercambia la ventana en segundo plano que se muestra y la reemplaza con la actividad principal. En este punto, el usuario puede comenzar a usar la app.

En la Figura 1, se muestra cómo los procesos del sistema y la app transfieren el trabajo entre sí.

Figura 1: Representación visual de las partes importantes del inicio en frío de una aplicación.

Pueden surgir problemas de rendimiento durante la creación de la app y de la actividad.

Creación de la aplicación

Cuando se inicia tu aplicación, la ventana en blanco de inicio permanece en la pantalla hasta que el sistema termina de abrir la app por primera vez. En ese momento, el proceso del sistema intercambia la ventana de inicio por tu app, lo que le permite al usuario comenzar a usarla.

Si sobrecargaste Application.onCreate() en tu propia app, el sistema invoca el método onCreate() en el objeto de tu app. Luego, la app genera el subproceso principal, también conocido como subproceso de IU, y le asigna la tarea de crear la actividad principal.

Desde este momento, los procesos del nivel del sistema y de la app continúan de acuerdo con las etapas del ciclo de vida de la app.

Creación de la actividad

Después de que el proceso de la app crea tu actividad, esta lleva a cabo las siguientes operaciones:

  1. Inicializa valores.
  2. Llama a constructores.
  3. Llama al método de devolución de llamada, como Activity.onCreate(), apropiado para el estado del ciclo de vida de ese momento de la actividad.

Por lo general, el método onCreate() produce el mayor impacto en el tiempo de carga, ya que realiza el mayor trabajo de sobrecarga: carga y aumenta vistas, e inicializa los objetos necesarios para que se ejecute la actividad.

Inicio semicaliente

El inicio semicaliente comprende un subconjunto de las operaciones que se llevan a cabo durante el inicio en frío. Al mismo tiempo, representa una sobrecarga mayor que en el caso del inicio en caliente. Muchos estados potenciales pueden ser considerados como inicios semicalientes. Por ejemplo:

  • El usuario cancela la operación de tu app, pero luego vuelve a iniciarla. La ejecución del proceso podría haber continuado, pero la app debe recrear la actividad desde cero llamando a onCreate().

  • El sistema quita la app de la memoria y, luego, el usuario vuelve a iniciarla. Es necesario reiniciar el proceso y la actividad, pero de algún modo, la tarea puede aprovechar el paquete de estado de la instancia guardada, que se pasó a onCreate().

Inicio en caliente

El inicio en caliente de la aplicación es mucho más simple y requiere menos sobrecarga que el inicio en frío. En un inicio en caliente, la única tarea que realiza el sistema es llevar la actividad al primer plano. Si todas las actividades de tu app están todavía en la memoria, la aplicación puede evitar tener que repetir la inicialización del objeto, el aumento del diseño y la renderización.

Sin embargo, si se borró una parte de la memoria en respuesta a eventos de recorte de memoria, como onTrimMemory(), será necesario recrear esos objetos como respuesta al evento de inicio en caliente.

El inicio en caliente muestra el mismo comportamiento en pantalla que el inicio en frío:

el proceso del sistema muestra una pantalla en blanco hasta que la app termina de renderizar la actividad.

Figura 2: En este diagrama, se muestran los diferentes estados de inicio y sus respectivos procesos; cada estado comienza desde el primer fotograma.

Cómo usar las métricas para detectar y diagnosticar problemas

Si quieres diagnosticar de manera adecuada el rendimiento del tiempo de inicio, puedes registrar métricas que muestren cuánto tarda tu aplicación en iniciarse. Android ofrece varios medios para avisarte cuando tu app tiene un problema y para ayudarte a diagnosticarlo. Android vitals puede alertarte cuando se produce un problema, y las herramientas de diagnóstico pueden ayudarte a diagnosticarlo.

Beneficios de usar métricas de inicio

Android usa las métricas Tiempo para la visualización inicial y Tiempo para la visualización completa a fin de optimizar los inicios de aplicaciones semicalientes y en frío. Android Runtime (ART) usa los datos de estas métricas para precompilar de forma eficiente a fin de optimizar los inicios en el futuro.

Los inicios más rápidos generan una interacción más prolongada de los usuarios con tu app, lo cual reduce las instancias en las que se sale de la app con antelación, se reinicia la instancia o se cambia a otra app.

Android vitals

Android vitals puede ayudarte a mejorar el rendimiento de tu app. Para ello, te envía alertas a través de Play Console cuando los tiempos de inicio de tu app son excesivos, como en los siguientes casos:

Android vitals usa la métrica Tiempo para la visualización inicial y no informa datos de los inicios en caliente. Si deseas obtener información sobre cómo Google Play recopila datos de Android vitals, consulta la documentación de Play Console.

Tiempo para la visualización inicial

La métrica Tiempo para la visualización inicial (TTID) mide el tiempo que tarda una aplicación en producir su primer fotograma, incluida la inicialización del proceso (si es un inicio en frío), la creación de la actividad (si es en frío o semicaliente) y la visualización del primer fotograma.

Cómo recuperar TTID

En Android 4.4 (nivel de API 19) y versiones posteriores, logcat incluye una línea de resultados que contiene un valor llamado Displayed. Este valor representa el tiempo transcurrido entre que se inició el proceso y se terminó de abrir la actividad correspondiente en la pantalla. El tiempo transcurrido comprende la siguiente secuencia de eventos:

  • Iniciar el proceso
  • Inicializar los objetos.
  • Crear e inicializar la actividad.
  • Aumentar el diseño.
  • Abrir la app por primera vez.

La línea de registro informada es similar al siguiente ejemplo:

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

Si realizas un seguimiento de resultados de logcat desde la línea de comandos, o en un terminal, encontrarás el tiempo transcurrido de manera sencilla. Para encontrar el tiempo transcurrido en Android Studio, debes inhabilitar los filtros en la vista de logcat. Esto es necesario porque el servidor del sistema, no la app misma, entrega este registro.

Una vez que establezcas los parámetros de configuración necesarios, podrás buscar con facilidad el término correcto para ver el tiempo. En la Figura 2, se muestra cómo inhabilitar los filtros y, en la segunda línea de resultados, comenzando desde la parte inferior, hay un ejemplo de resultado de logcat en relación con el tiempo Displayed.

Figura 2: Inhabilitación de filtros y búsqueda del valor "Displayed" en logcat.

La métrica Displayed de los resultados de logcat no necesariamente captura el tiempo transcurrido hasta que se cargan y muestran todos los recursos: omite los recursos a los que no se hace referencia en el archivo de diseño o que la app crea como parte de la inicialización del objeto. Excluye estos recursos porque cargarlos es un proceso intercalado, y no se bloquea la pantalla inicial de la app.

En ocasiones, la línea Displayed de los resultados de logcat incluye un campo adicional para el tiempo total. Por ejemplo:

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

En este caso, la primera medición de tiempo se realiza solo para la actividad que se inició primero. La medición de tiempo total comienza cuando se inicia el proceso de la app y podría incluir otra actividad que se haya iniciado primero, pero que no mostró nada en la pantalla. Solo se muestra la medición de tiempo total cuando hay una diferencia entre los tiempos de inicio de la actividad y el tiempo total de inicio.

También puedes medir el tiempo transcurrido hasta la visualización inicial ejecutando la app con el comando del Administrador de actividades de ADB Shell. Por ejemplo:

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

La métrica Displayed aparece en los resultados de logcat, como antes. En tu ventana de terminal, también se debería mostrar lo siguiente:

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

Los argumentos -c y -a son opcionales y te permiten especificar <category> y <action>.

Tiempo para la visualización completa

La métrica Tiempo para la visualización completa (TTFD) mide el tiempo que tarda la aplicación en producir su primer fotograma con contenido completo, incluido el contenido que se carga de forma asíncrona después del primer fotograma. Por lo general, es el contenido principal de lista que se carga desde la red, según lo informa la app.

Cómo recuperar TTFD

Puedes usar el método reportFullyDrawn() para medir el tiempo transcurrido entre el inicio de la aplicación y la visualización completa de todos los recursos y las jerarquías de vistas, lo que puede ser útil en casos en los que una app realiza una carga diferida. En estos casos, la app no bloquea la apertura inicial de la ventana, pero carga recursos de manera asíncrona y actualiza la jerarquía de vistas.

Si, debido a la descarga diferida, la visualización inicial de la app no incluye todos los recursos, puedes considerar la carga completa y la visualización de todos los recursos y vistas como una métrica separada. Por ejemplo, tu IU podría estar completamente cargada, incluso con algo de texto, pero aún no mostrar imágenes que la app debe tomar de la red.

Para abordar este problema, puedes llamar a reportFullyDrawn() de forma manual a fin de indicarle al sistema que la actividad finalizó gracias a la carga diferida. Cuando usas este método, el valor que muestra logcat es el tiempo transcurrido entre la creación del objeto de la aplicación y el momento en que se llama a reportFullyDrawn(). El siguiente es un ejemplo de resultado de logcat:

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

En ocasiones, el resultado de logcat incluye un tiempo total, como se describe en Tiempo para la visualización inicial.

Si detectas que los tiempos de visualización son más lentos de lo que esperabas, puedes intentar identificar los cuellos de botella en el proceso de inicio.

Cómo identificar cuellos de botella

Un buen método para buscar cuellos de botellas es usar la herramienta Generador de perfiles de CPU de Android Studio. Para obtener más información, consulta Cómo inspeccionar la actividad de la CPU con el Generador de perfiles de CPU.

Además, puedes obtener información sobre los posibles cuellos de botella mediante el seguimiento intercalado de los métodos onCreate() de tus apps y actividades. Para obtener más información sobre el seguimiento intercalado, consulta la documentación sobre las funciones de Trace y la descripción general del registro del sistema.

Cómo estar al tanto de los problemas habituales

En esta sección, se analizan varios problemas que, a menudo, afectan el rendimiento de inicio de las apps. Por lo general, estos problemas están relacionados con la inicialización de apps y objetos de actividades, además de con la carga de pantallas.

Inicialización de app intensa

El rendimiento del inicio puede verse afectado cuando tu código anula el objeto Application y ejecuta trabajo pesado o lógica compleja cuando se inicializa ese objeto. Es posible que tu app pierda tiempo durante el inicio si las subclases de Application llevan a cabo inicializaciones que aún no son necesarias. Algunas de estas pueden ser completamente innecesarias; por ejemplo, inicializar información de estado para la actividad principal, cuando la app ya se inició en respuesta a un intent. Con un intent, la app usa solo un subconjunto de los datos de estado inicializados con anterioridad.

Otros desafíos que se presentan durante la inicialización de una app incluyen eventos de recolección de elementos no utilizados, que tienen un gran impacto o son numerosos, o E/S de disco que se producen junto con la inicialización y bloquean el proceso aún más. La recolección de elementos no utilizados se debe tener en cuenta específicamente para el tiempo de ejecución de Dalvik; el tiempo de ejecución de Art realiza la recolección de elementos no utilizados al mismo tiempo, lo que minimiza el impacto de esa operación.

Cómo diagnosticar el problema

Puedes usar el registro de métodos o el registro en línea para diagnosticar el problema.

Seguimiento de métodos

Cuando se ejecuta el Generador de perfiles de CPU, se descubre que el método callApplicationOnCreate() llama finalmente a tu método com.example.customApplication.onCreate. Si la herramienta muestra que la ejecución de estos métodos tarda demasiado en completarse, deberías seguir explorando para descubrir qué trabajo se está desarrollando.

Seguimiento intercalado

Usa el seguimiento intercalado para investigar posibles culpables, por ejemplo:

  • La función onCreate() inicial de tu app
  • Cualquier objeto singleton que tu app inicializa
  • Cualquier E/S de disco, deserialización o bucle cerrado que pueda producirse durante el cuello de botella

Soluciones para el problema

Si el problema se relaciona con inicializaciones innecesarias o E/S de disco, la solución es la inicialización diferida. En otras palabras, solo deberías inicializar los objetos que son inmediatamente necesarios. En lugar de crear objetos globales estáticos, cambia a un patrón singleton, en el que la app inicializa objetos solo la primera vez que los necesita.

Además, considera usar un framework de inyección de dependencias, como Hilt, que crea objetos y dependencias cuando se los inyecta por primera vez.

Si la app usa proveedores de contenido para inicializar los componentes de la app al inicio, considera usar la biblioteca de App Startup.

Inicialización de actividad intensa

A menudo, la creación de actividades implica mucho trabajo de sobrecarga alta. En general, hay oportunidades de optimizar este trabajo para obtener mejoras de rendimiento. Entre este tipo de problemas habituales, se incluyen los siguientes:

  • Ampliación de diseños grandes o complejos.
  • Bloqueo de la pantalla cuando se escribe en el disco, o E/S de red.
  • Carga y decodificación de mapas de bits
  • Rasterización de objetos VectorDrawable
  • Inicialización de otros subsistemas de la actividad

Cómo diagnosticar el problema

También en este caso, tanto el registro de métodos como el registro en línea pueden ser útiles.

Seguimiento de métodos

Cuando uses el Generador de perfiles de CPU, presta atención a los constructores de la subclase Application y los métodos com.example.customApplication.onCreate() de tu app.

Si la herramienta muestra que la ejecución de estos métodos tarda demasiado en completarse, deberías seguir explorando para descubrir qué trabajo se está desarrollando.

Seguimiento intercalado

Usa el seguimiento intercalado para investigar posibles culpables, por ejemplo:

  • La función onCreate() inicial de tu app
  • Cualquier objeto singleton que tu app inicializa
  • Cualquier E/S de disco, deserialización o bucle cerrado que pueda producirse durante el cuello de botella

Soluciones para el problema

Si bien hay varios cuellos de botella posibles, los siguientes son dos problemas y soluciones habituales:

  • Cuando más grande es tu jerarquía de vistas, más tarda la app en ampliarla. Puedes seguir dos pasos para abordar este problema:

    • Acopla la jerarquía de vistas reduciendo los diseños redundantes o anidados.

    • No amplíes partes IU que no sea necesario mostrar durante el inicio. En su lugar, usa un objeto ViewStub como marcador de posición para subjerarquías que la app pueda ampliar en un tiempo más apropiado.

  • Tener toda la inicialización de recursos en el subproceso principal también puede hacer que el inicio sea más lento. Puedes tratar este problema de la siguiente manera:

    • Transfiere toda la inicialización de recursos para que la app pueda llevarla a cabo de manera diferida en otro subproceso.
    • Permite que la app cargue y muestre las vistas y, luego, actualiza las propiedades visuales que dependen de mapas de bits y otros recursos.

Pantallas de presentación personalizadas

Es posible que observes tiempo adicional durante el inicio si ya usaste uno de los siguientes métodos para implementar una pantalla de presentación personalizada en Android 11 (nivel de API 30) o versiones anteriores:

  • Usar el atributo del tema windowDisablePreview para desactivar la pantalla en blanco inicial que dibuja el sistema durante el lanzamiento

  • Usar una actividad dedicada

A partir de Android 12, se requiere migrar a la API de SplashScreen. Esta API permite un tiempo de inicio más rápido y te permite ajustar la pantalla de presentación de las siguientes maneras:

Además, la biblioteca de compatibilidad proporciona portabilidad a versiones anteriores de la API de SplashScreen a fin de permitir la retrocompatibilidad y crear una apariencia coherente para la visualización de la pantalla de presentación en todas las versiones de Android.

Consulta la guía de migración de la pantalla de presentación para obtener más detalles.