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.

Cómo interpretar los aspectos internos del inicio de una 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 en caliente lento 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 en caliente lento y en caliente.

Para 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 el 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 app

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

Creación de la app

Cuando se inicia tu app, 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 actual 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 en caliente

El inicio en caliente de la app 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 app 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 rápido.

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.

Inicio en caliente lento

El inicio en caliente lento 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 en caliente lentos. 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().

Cómo detectar y diagnosticar problemas

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.

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 tu app presenta una cantidad excesiva de fallas. Android vitals considera que el tiempo de inicio de tu app es excesivo en los siguientes casos:

Una sesión diaria corresponde a un día en el que se usó tu app.

Android vitals no informa datos sobre 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.

Cómo diagnosticar tiempos de inicio lento

Si quieres diagnosticar de manera adecuada el rendimiento del tiempo de inicio, puedes registrar métricas que muestren cuánto tarda tu app en iniciarse.

Tiempo para la visualización inicial

En Android 4.4 (API nivel 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:

  1. Iniciar el proceso.
  2. Inicializar los objetos.
  3. Crear e inicializar la actividad.
  4. Aumentar el diseño.
  5. 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 hagas los ajustes 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: Cómo inhabilitar los filtros y buscar el 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 en línea, 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> para el intent.

Tiempo para la visualización completa

Puedes usar el método reportFullyDrawn() para medir el tiempo transcurrido entre el inicio de la app y la visualización completa de todos los recursos y las jerarquías de vistas. Esto 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 para indicarle al sistema que la actividad finalizó con su carga diferida. Cuando usas este método, el valor que muestra logcat es el tiempo transcurrido entre la creación del objeto de la app 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 CPU Profiler de Android Studio. Para obtener más información, consulta Cómo inspeccionar la actividad de la CPU con CPU Profiler.

Además, puedes obtener información sobre los posibles cuellos de botella mediante el registro en línea de los métodos onCreate() de tus apps y actividades. Para obtener más información sobre el registro en línea, 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 comunes

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 al inicializar 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.

Registro de métodos

Cuando se ejecuta CPU Profiler, 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.

Registro en línea

Usa el registro en línea para investigar posibles culpables, por ejemplo:

  • La función onCreate() inicial de tu app.
  • Cualquier objeto singleton que tu app inicializa.
  • Cualquier E/S, 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 requiere objetos de inicialización diferida: solo inicializar los objetos que son inmediatamente necesarios. Por ejemplo, en lugar de crear objetos globales estáticos, usa un patrón singleton, en el que la app inicializa objetos solo la primera vez que accede a ellos. Además, considera usar un marco de trabajo de inyección de dependencias, como Dagger, que crea objetos y dependencias cuando se los inyecta por primera vez.

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 comunes, 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.

Registro de métodos

Cuando uses CPU Profiler, 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.

Registro en línea

Usa el registro en línea para investigar posibles culpables, por ejemplo:

  • La función onCreate() inicial de tu app.
  • Cualquier objeto singleton que tu app inicializa.
  • Cualquier E/S, 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 comunes:

  • 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 de la IU que no sea necesario que se vean durante el inicio. En cambio, 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 inicio con temas

Es posible que quieras aplicar un tema a la experiencia de carga de tu app, para que la pantalla de inicio de esta tenga un tema coherente con el resto de la app, en lugar de con el tema del sistema. Hacer esto puede ocultar un inicio de actividad lento.

Un modo común de implementar una pantalla de inicio con tema es usar el atributo del tema windowDisablePreview para desactivar la pantalla inicial en blanco que el proceso del sistema muestra cuando inicia la app. Sin embargo, este enfoque puede hacer que el tiempo de inicio sea mayor que en apps que no suprimen la ventana de vista previa. Por otra parte, obliga al usuario a esperar sin respuesta mientras se inicia la actividad, por lo que se preguntará si la app está funcionando correctamente.

Cómo diagnosticar el problema

En general, puedes diagnosticar este problema si detectas una respuesta lenta cuando un usuario inicia tu app. En ese caso, es posible que la pantalla parezca bloqueada, o que dejó de responder a las entradas.

Soluciones para el problema

Te recomendamos que, en lugar de inhabilitar la ventana de vista previa, sigas los patrones comunes de Material Design. Puedes usar el atributo del tema windowBackground de la actividad para proporcionar un elemento de diseño simple y personalizado para la actividad de inicio.

Por ejemplo, puedes crear un nuevo archivo de elemento de diseño y hacer referencia a él desde el archivo XML de diseño y el archivo del manifiesto de la app, de la siguiente manera:

Archivo XML de diseño:

    <layer-list xmlns:android="http://schemas.android.com/apk/res/android" android:opacity="opaque">
      <!-- The background color, preferably the same as your normal theme -->
      <item android:drawable="@android:color/white"/>
      <!-- Your product logo - 144dp color version of your app icon -->
      <item>
        <bitmap
          android:src="@drawable/product_logo_144dp"
          android:gravity="center"/>
      </item>
    </layer-list>
    

Archivo de manifiesto

    <activity ...
    android:theme="@style/AppTheme.Launcher" />
    

La manera más fácil de volver al modo normal es llamar a setTheme(R.style.AppTheme) antes de llamar a super.onCreate() y setContentView():

Kotlin

    class MyMainActivity : AppCompatActivity() {

        override fun onCreate(savedInstanceState: Bundle?) {
            // Make sure this is before calling super.onCreate
            setTheme(R.style.Theme_MyApp)
            super.onCreate(savedInstanceState)
            // ...
        }
    }
    

Java

    public class MyMainActivity extends AppCompatActivity {
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        // Make sure this is before calling super.onCreate
        setTheme(R.style.Theme_MyApp);
        super.onCreate(savedInstanceState);
        // ...
      }
    }