Cómo optimizar tu velocidad de compilación

Los tiempos de compilación prolongados ralentizan el proceso de desarrollo. En esta página, se ofrecen algunas técnicas para ayudarte a resolver cuellos de botella en la velocidad de compilación.

El proceso general de mejora de la velocidad de compilación es el siguiente:

  1. Optimizar la configuración de tu compilación mediante algunos pasos que proporcionan beneficios inmediatos para la mayoría de los proyectos de Android Studio
  2. Generar un perfil para tu compilación a fin de identificar y diagnosticar algunos de los cuellos de botella más complejos que pueden ser específicos de tu proyecto o estación de trabajo

Al desarrollar tu app, debes implementar un dispositivo con Android 7.0 (nivel de API 24) o una versión posterior siempre que sea posible. En las últimas versiones de la plataforma Android, se implementan mejores mecanismos para introducir actualizaciones en tu app, como el tiempo de ejecución de Android (ART) y la compatibilidad nativa con varios archivos DEX.

Nota: Después de tu primera compilación limpia, probablemente observes que las compilaciones posteriores, limpias e incrementales, son mucho más rápidas (aun cuando no se use ninguna de las optimizaciones que se describen en esta página). Esto se debe a que el daemon de Gradle tiene un período de “calentamiento” en el que aumenta el rendimiento, fenómeno similar al de otros procesos de equipos virtuales Java (JVM).

Cómo optimizar la configuración de tu compilación

Sigue estas sugerencias para mejorar la velocidad de compilación de tu proyecto de Android Studio.

Mantén tus herramientas actualizadas

Las herramientas de Android reciben optimizaciones de compilación y funciones nuevas prácticamente con todas las actualizaciones, y, para algunas de las sugerencias que encontrarás en esta página, se presupone que usas la última versión. Para aprovechar las últimas optimizaciones, mantén actualizado lo siguiente:

Crea una variante de compilación para desarrollo

Muchas de las configuraciones que necesitas al preparar tu app para el lanzamiento no son necesarias mientras la desarrollas. Al habilitar procesos de compilación innecesarios, las compilaciones incrementales y limpias se vuelven más lentas, por lo que debes configurar una variante de compilación que conserve solo las configuraciones de compilación que necesitas para el desarrollo de tu app. En el siguiente ejemplo, se crean un tipo “dev” y un tipo “prod” (para las configuraciones de tu versión de lanzamiento):

    android {
      ...
      defaultConfig {...}
      buildTypes {...}
      productFlavors {
        // When building a variant that uses this flavor, the following configurations
        // override those in the defaultConfig block.
        dev {
          // To avoid using legacy multidex when building from the command line,
          // set minSdkVersion to 21 or higher. When using Android Studio 2.3 or higher,
          // the build automatically avoids legacy multidex when deploying to a device running
          // API level 21 or higher—regardless of what you set as your minSdkVersion.
          minSdkVersion 21
          versionNameSuffix "-dev"
          applicationIdSuffix '.dev'
        }

        prod {
          // If you've configured the defaultConfig block for the release version of
          // your app, you can leave this block empty and Gradle uses configurations in
          // the defaultConfig block instead. You still need to create this flavor.
          // Otherwise, all variants use the "dev" flavor configurations.
        }
      }
    }
    

Si en la configuración de tu compilación ya se usan tipos de productos para crear diferentes versiones de tu app, puedes combinar las configuraciones “dev” y “prod” con esos tipos usando dimensiones de tipos. Por ejemplo, si ya configuraste tipos “demo” y “full”, puedes usar el siguiente ejemplo de configuración para crear tipos combinados, como “devDemo” y “prodFull”:

    android {
      ...
      defaultConfig {...}
      buildTypes {...}

      // Specifies the flavor dimensions you want to use. The order in which you
      // list each dimension determines its priority, from highest to lowest,
      // when Gradle merges variant sources and configurations. You must assign
      // each product flavor you configure to one of the flavor dimensions.

      flavorDimensions "stage", "mode"

      productFlavors {
        dev {
          dimension "stage"
          minSdkVersion 21
          versionNameSuffix "-dev"
          applicationIdSuffix '.dev'
          ...
        }

        prod {
          dimension "stage"
          ...
        }

        demo {
          dimension "mode"
          ...
        }

        full {
          dimension "mode"
          ...
        }
      }
    }
    

Habilita la sincronización de proyectos con una sola variante

Sincronizar tu proyecto con la configuración de compilación es un paso importante para que Android Studio pueda comprender la manera en que está estructurado tu proyecto. Sin embargo, en el caso de proyectos grandes, el proceso puede consumir mucho tiempo. Si tu proyecto usa múltiples variantes de compilación, ahora puedes optimizar las sincronizaciones limitándolas solo a la variante que hayas seleccionado.

Debes usar Android Studio 3.3 o una versión posterior con el complemento Gradle para Android 3.3.0 o una versión posterior para habilitar esta optimización. La optimización está habilitada de forma predeterminada en todos los proyectos.

Para habilitarla manualmente, haz clic en File > Settings > Experimental > Gradle (Android Studio > Preferences > Experimental > Gradle en Mac) y selecciona la casilla de verificación Only sync the active variant.

Nota: Esta optimización admite proyectos que incluyen los lenguajes Java y C++, y tiene algo de compatibilidad para Kotlin. Al habilitar la optimización para proyectos con contenido de Kotlin, la sincronización de Gradle vuelve a usar variantes completas internamente.

Evita compilar recursos innecesarios

Evita compilar y empaquetar recursos que no pruebes (como localizaciones de idioma adicionales y recursos de densidad de pantalla). También puedes hacerlo especificando únicamente un recurso de idioma y una densidad de pantalla para el tipo “dev”, como se muestra en el siguiente ejemplo:

    android {
      ...
      productFlavors {
        dev {
          ...
          // The following configuration limits the "dev" flavor to using
          // English stringresources and xxhdpi screen-density resources.
          resConfigs "en", "xxhdpi"
        }
        ...
      }
    }
    

Inhabilita Crashlytics para tus compilaciones de depuración

Si no necesitas ejecutar un informe de Crashlytics, acelera tus compilaciones de depuración inhabilitando el complemento de la siguiente manera:

    android {
      ...
      buildTypes {
        debug {
          ext.enableCrashlytics = false
        }
    }
    

También debes inhabilitar el kit de Crashlytics en el tiempo de ejecución para las compilaciones de depuración cambiando la manera en la que inicializas la compatibilidad con Fabric en tu app, como se muestra a continuación:

Kotlin

    // Initializes Fabric for builds that don't use the debug build type.
    Crashlytics.Builder()
            .core(CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build())
            .build()
            .also { crashlyticsKit ->
                Fabric.with(this, crashlyticsKit)
            }
    

Java

    // Initializes Fabric for builds that don't use the debug build type.
    Crashlytics crashlyticsKit = new Crashlytics.Builder()
        .core(new CrashlyticsCore.Builder().disabled(BuildConfig.DEBUG).build())
        .build();

    Fabric.with(this, crashlyticsKit);
    

Si deseas usar Crashlytics con tus compilaciones de depuración, puedes de todos modos acelerar las compilaciones incrementales evitando que Crashlytics actualice recursos de la app con su ID de compilación único durante cada compilación. Para evitar que Crashlytics actualice todo el tiempo su ID de compilación, agrega lo siguiente a tu archivo build.gradle:

    android {
      ...
      buildTypes {
        debug {
          ext.alwaysUpdateBuildId = false
        }
    }
    

Para obtener más información sobre la optimización de tus compilaciones mientras usas Crashlytics, lee la documentación oficial.

Usa valores de configuración de compilación estáticos con tu compilación de depuración

Usa siempre valores estáticos o no modificables para las propiedades que se incluyen en el archivo de manifiesto o los archivos de recursos para tu tipo de compilación de depuración. Si los valores de tu archivo de manifiesto o los recursos de tu app deben actualizarse con cada compilación, Instant Run no podrá cambiar el código; deberá compilar e instalar un APK nuevo.

Por ejemplo, para usar códigos de versión dinámicos, nombres de versión, recursos o cualquier otra lógica de compilación que modifique el archivo de manifiesto, se requiere una compilación del APK completa cada vez que desees ejecutar un cambio, aunque en otras circunstancias la modificación solo hubiera requerido un intercambio directo. Si tu configuración de compilación requiere esas propiedades dinámicas, debes aislarlas de las variantes de tu compilación de lanzamiento y conservar valores estáticos para tus compilaciones de depuración, como se muestra en el archivo build.gradle a continuación.

    int MILLIS_IN_MINUTE = 1000 * 60
    int minutesSinceEpoch = System.currentTimeMillis() / MILLIS_IN_MINUTE

    android {
        ...
        defaultConfig {
            // Making either of these two values dynamic in the defaultConfig will
            // require a full APK build and reinstallation because the AndroidManifest.xml
            // must be updated (which is not supported by Instant Run).
            versionCode 1
            versionName "1.0"
            ...
        }

        // The defaultConfig values above are fixed, so your incremental builds don't
        // need to rebuild the manifest (and therefore the whole APK, slowing build times).
        // But for release builds, it's okay. So the following script iterates through
        // all the known variants, finds those that are "release" build types, and
        // changes those properties to something dynamic.
        applicationVariants.all { variant ->
            if (variant.buildType.name == "release") {
                variant.mergedFlavor.versionCode = minutesSinceEpoch;
                variant.mergedFlavor.versionName = minutesSinceEpoch + "-" + variant.flavorName;
            }
        }
    }
    

Usa versiones de dependencias estáticas

Cuando declares dependencias en tus archivos build.gradle, debes evitar usar números de versión con un signo más al final, como 'com.android.tools.build:gradle:2.+'. El uso de números de versión dinámicos puede generar actualizaciones de versión inesperadas, dificultar la resolución de diferencias de versión y ralentizar las compilaciones debido a que Gradle busca actualizaciones. Como alternativa, debes usar números de versión estáticos o no modificables.

Habilita el modo sin conexión

Si tu conexión de red es lenta, los tiempos de compilación pueden verse afectados cuando Gradle intente usar recursos de la red para resolver dependencias. Puedes indicar a Gradle que evite usar recursos de red y use solo los artefactos almacenados en caché a nivel local.

Para usar Gradle sin conexión cuando realices compilaciones con Android Studio, haz lo siguiente:

  1. Abre la ventana Preferences haciendo clic en File > Settings (en Mac, Android Studio > Preferences).
  2. En el subpanel izquierdo, haz clic en Build, Execution, Deployment > Gradle.
  3. Selecciona la casilla de verificación Offline work.
  4. Haz clic en Apply o en OK.

Si estás compilando desde la línea de comando, pasa la opción --offline.

Crea módulos de biblioteca

Busca en tu app código que puedas convertir en un módulo de biblioteca de Android. La modularización de tu código de esta manera permite que el sistema de compilación compile solo los módulos que modifiques y almacene en caché los resultados para compilaciones futuras. También aporta más eficacia a la ejecución de proyectos paralelos (cuando habilitas esa optimización).

Crea tareas para lógica de compilación personalizada

Una vez que crees un perfil para la compilación, si en este se muestra que una fracción relativamente larga del tiempo de compilación se dedica a la etapa de “configuración de proyectos”, revisa tus secuencias de comandos de build.gradle y busca código que puedas incluir en una tarea de Gradle personalizada. Al trasladar parte de la lógica de compilación a una tarea, que se ejecutará únicamente cuando sea necesario, los resultados se pueden almacenar en caché para compilaciones futuras y la lógica de compilación reunirá las condiciones para ejecutarse en paralelo (si habilitas la ejecución de objetos en paralelo). Para obtener más información, lee la documentación oficial de Gradle.

Sugerencia: Si tu compilación incluye una gran cantidad de tareas personalizadas, te recomendamos ordenar tus archivos build.gradle creando clases de tareas personalizadas. Agrega tus clases al directorio project-root/buildSrc/src/main/groovy/, y Gradle los incluirá automáticamente en la classpath para todos los archivos build.gradle de tu proyecto.

Convierte imágenes a WebP

WebP es un formato de archivo de imagen que proporciona compresión con pérdida de información (como JPEG) y transparencia (como PNG), pero puede ofrecer mejor compresión que estos otros dos formatos. Reducir los tamaños de los archivos de imagen sin necesidad de realizar compresión de tiempo de compilación puede acelerar tus compilaciones, en particular si tu app usa una gran cantidad de recursos de imagen. No obstante, puedes observar un pequeño incremento en el uso de la CPU del dispositivo mientras se descomprimen las imágenes WebP. Con Android Studio, puedes convertir tus imágenes a WebP fácilmente.

Inhabilita la compresión de PNG

Si no puedes (o no deseas) convertir tus imágenes PNG a WebP, puedes de todos modos acelerar tu compilación inhabilitando la compresión automática de imágenes cada vez que compilas tu app. Si usas un complemento de Android para Gradle 3.0.0 o versiones posteriores, la compresión de PNG está inhabilitada de forma predeterminada solo para el tipo de compilación "debug". Para deshabilitar esta optimización en otros tipos de compilación, agrega lo siguiente a tu archivo build.gradle:

    android {
        buildTypes {
            release {
                // Disables PNG crunching for the release build type.
                crunchPngs false
            }
        }

    // If you're using an older version of the plugin, use the
    // following:
    //  aaptOptions {
    //      cruncherEnabled false
    //  }
    }
    

Debido a que los tipos de compilación o los tipos de productos no definen esta propiedad, debes establecerla manualmente en true cuando compiles la versión de lanzamiento de tu app.

Habilita Instant Run

Instant Run reduce notablemente el tiempo de actualización de tu app al aplicar determinados cambios en el código y los recursos sin compilar un APK nuevo y, en algunos casos, sin reiniciar la actividad actual. Para usar Instant Run, haz clic en Apply Changes después de hacer cambios en tu app. Instant Run también se habilita de forma predeterminada cuando haces lo siguiente:

  • Compilas tu app usando una variante de compilación de depuración.
  • Usas la versión 2.3.0 o una posterior del complemento de Android para Gradle.
  • Estableces minSdkVersion en 15 o más en el archivo build.gradle de nivel de módulo de tu app.
  • Implementas tu app en un dispositivo con Android 5.0 (nivel de API 21) y versiones posteriores haciendo clic en Run .

Si no ves el botón Apply Changes en la barra de herramientas después de cumplir con estos requisitos, asegúrate de que Instant Run no esté inhabilitado en la configuración de IDE. Para esto, aplica los pasos siguientes:

  1. Abre el diálogo Settings o Preferences.
  2. Dirígete a Build, Execution, Deployment > Instant Run.
  3. Asegúrate de que Enable Instant Run esté seleccionado.

Habilita la caché de compilación

La caché de compilación guarda determinada información de salida que genera el complemento de Android para Gradle al compilar tu proyecto (como AAR sin empaquetar y dependencias remotas previamente convertidas a archivos Dex). Tus compilaciones limpias son mucho más rápidas mientras se usa la caché, ya que el sistema de compilación puede simplemente reutilizar esos archivos almacenados en caché en compilaciones posteriores en lugar de volver a crearlos.

Los proyectos nuevos que usan el complemento de Android 2.3.0 y versiones posteriores habilitan la caché de compilación de forma predeterminada (a menos que inhabilites la caché de compilación de forma explícita). Para obtener más información, lee Cómo acelerar compilaciones limpias con la caché de compilación.

Utiliza procesadores de anotaciones graduales

La versión 3.3.0 del complemento Gradle para Android y las versiones superiores mejoran la compatibilidad con la compilación gradual de Java al usar procesadores de anotaciones. Por lo tanto, para mejorar la velocidad de las compilaciones graduales, debes actualizar el complemento Gradle para Android y utilizar solo procesadores de anotación graduales siempre que sea posible.

Nota: Esta función es compatible con la versión 4.10.1 de Gradle y con las superiores, excepto para Gradle 5.1 (consulta el problema de Gradle número 8194).

Para comenzar, consulta la siguiente lista de procesadores de anotación populares que admiten la compilación gradual de Java. Para obtener una lista más completa, sigue el problema número 5277 de GitHub. Es posible que algunos de los complementos requieran pasos adicionales para habilitar la optimización, así que asegúrate de leer la documentación de cada uno.

Ten en cuenta que, si usas uno o más procesadores de anotación que no admiten compilaciones graduales, la compilación gradual de Java no está habilitada. Si necesitas usar un procesador de anotaciones que no admita esta optimización, incluye el siguiente indicador en tu archivo gradle.properties. Al hacerlo, el complemento Gradle de Android ejecuta todos los procesadores de anotación en una tarea separada y permite que las tareas de compilación de Java se ejecuten de forma gradual.

    android.enableSeparateAnnotationProcessing = true
    

Cómo generar un perfil para tu compilación

Los proyectos más grandes, o aquellos que implementan una gran cantidad de lógica de compilación predeterminada, pueden exigir que indagues más en el proceso de compilación para detectar cuellos de botella. Puedes hacerlo generando perfiles sobre el tiempo que Gradle tarda en ejecutar cada etapa del ciclo de vida de la compilación y cada tarea de compilación. Por ejemplo, si tu perfil de compilación muestra que Gradle tarda mucho tiempo en configurar tu proyecto, esto podría sugerir que necesitas mover lógica de compilación personalizada fuera de la etapa de configuración. Además, si la tarea mergeDevDebugResources consume una gran cantidad de tiempo de compilación, esto puede indicar que necesitas convertir tus imágenes a WebP o inhabilitar la compresión de PNG.

Usar la generación de perfiles para mejorar la velocidad de la compilación, normalmente, implica ejecutar tu compilación con la generación de perfiles habilitada, realizar algunos ajustes en la configuración de la compilación y generar más perfiles para observar los resultados de tus cambios.

Para generar y ver un perfil de compilación, realiza los siguientes pasos:

  1. Con tu proyecto abierto en Android Studio, selecciona View > Tool Windows > Terminal para abrir una línea de comandos en la raíz de tu proyecto.
  2. Realiza una compilación limpia ingresando el siguiente comando. A medida que generes el perfil de tu compilación, deberás realizar una compilación limpia entre cada compilación de la que generes perfiles, ya que Gradle omite tareas cuando las entradas de estas (como el código fuente) no cambian. De este modo, una segunda compilación sin cambios en las entradas siempre se ejecuta más rápido porque las tareas no se vuelven a ejecutar. Por lo tanto, ejecutar la tarea clean entre tus compilaciones garantiza que generes un perfil del proceso de compilación completo.
        // On Mac or Linux, run the Gradle wrapper using "./gradlew".
        gradlew clean
        
  3. Ejecuta una compilación de depuración de uno de tus tipos de productos, como el tipo “dev”, con los siguientes indicadores:
        gradlew --profile --offline --rerun-tasks assembleFlavorDebug
        
    • --profile: Habilita la generación de perfiles.
    • --offline: Inhabilita la obtención de dependencias en línea por parte de Gradle. Esto garantiza que las demoras provocadas por Gradle cuando intenta actualizar tus dependencias no interfieran en los datos de la generación de perfiles. Para poder asegurarte de que Gradle haya descargado y almacenado en caché tus dependencias, es necesario que hayas compilado tu proyecto una vez.
    • --rerun-tasks: Hace que Gradle, de manera forzosa, vuelva a ejecutar todas las tareas e ignore las optimizaciones de tareas.
  4. Figura 1: Vista del proyecto que indica la ubicación de los informes del perfil

    Una vez finalizada la compilación, usa la ventana Project para dirigirte al directorio project-root/build/reports/profile/ (como se muestra en la figura 1).

  5. Haz clic con el botón secundario en el archivo profile-timestamp.html y selecciona Open in Browser > Default. El informe debe ser similar al que se muestra en la figura 2. Puedes inspeccionar cada pestaña de este para obtener información sobre tu compilación, como la pestaña Task Execution, en la que se muestra el tiempo que Gradle tardó en ejecutar cada tarea de compilación.

    Figura 2: Visualización de un informe en un navegador

  6. Opcional: Antes de realizar cambios en la configuración de tu proyecto o compilación, repite el comando del paso 3, pero omite el indicador --rerun-tasks. Debido a que Gradle intenta ahorrar tiempo al no volver a ejecutar tareas cuyas entradas no han cambiado (se indican como UP-TO-DATE en la pestaña Task Execution del informe, como se muestra en la figura 3), puedes identificar las tareas en ejecución que no deberían estar activas. Por ejemplo, si :app:processDevUniversalDebugManifest no se marca como UP-TO-DATE, esto puede sugerir que la configuración de tu compilación actualiza el manifiesto de forma dinámica con cada compilación. No obstante, algunas tareas deben ejecutarse durante cada compilación, como :app:checkDevDebugManifest.

    Figura 3: Visualización de los resultados de ejecución de tareas

Ahora que tienes un informe de perfil de compilación, puedes comenzar a buscar oportunidades de optimización al inspeccionar la información de cada pestaña del informe. Para algunas configuraciones de la compilación, se requiere experimentación, ya que los beneficios pueden diferir entre proyectos y estaciones de trabajo. Por ejemplo, usando ProGuard para quitar el código que no se utilice y reducir el tamaño del APK, se pueden obtener beneficios en proyectos que tengan una base de código grande. No obstante, es posible lograr más beneficios para proyectos más pequeños mediante la inhabilitación de ProGuard. Además, aumentar el tamaño del montón de Gradle (usando org.gradle.jvmargs) puede afectar negativamente el rendimiento de equipos con poca memoria.

Después de realizar un cambio en la configuración de tu compilación, observa los resultados de este repitiendo los pasos anteriores y generando un nuevo perfil de compilación. Por ejemplo, en la figura 4 se muestra un informe para el mismo ejemplo de app después de aplicar algunas de las optimizaciones básicas descritas en esta página.

Figura 4: Visualización de un informe nuevo después de optimizar la velocidad de compilación

Sugerencia: Si buscas una herramienta de generación de perfiles más eficaz, considera usar el generador de perfiles de código abierto de Gradle.