Cómo habilitar multidex para apps con una cantidad de métodos superior a 64k

Cuando tu app y las bibliotecas a las que hace referencia superan los 65,536 métodos, se produce un error de compilación que indica que la app alcanzó el límite de la arquitectura de compilación de Android:

trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.

Las versiones anteriores del sistema de compilación informan un error diferente, lo cual indica el mismo problema:

Conversion to Dalvik format failed:
Unable to execute dex: method ID not in [0, 0xffff]: 65536

Ambas condiciones de error muestran un número común: 65,536. Este número representa la cantidad total de referencias que el código puede invocar en un mismo archivo de código de bytes Dalvik Executable (DEX). En esta página, se explica la manera de superar este límite mediante la habilitación de una configuración de app conocida como multidex, que permite que tu app compile y lea varios archivos DEX.

Información sobre el límite de referencia de 64k

Los archivos de las apps para Android (APK) contienen archivos de código de bytes ejecutables con formato de archivos Dalvik Executable (DEX), que tienen el código compilado que se usa para ejecutar tu app. La especificación de Dalvik Executable limita a 65,536 la cantidad total de métodos a los que se puede hacer referencia en un archivo DEX, incluidos los métodos de framework de Android, de biblioteca y de tu propio código. En el contexto de la informática, el término kilo, y su símbolo, k, denotan 1,024 (o 2^10). Como 65,536 es igual a 64 x 1,024, este límite se denomina "límite de referencia de 64k".

Compatibilidad de multidex con versiones anteriores a Android 5.0

Las versiones de la plataforma anteriores a Android 5.0 (API nivel 21) usan el tiempo de ejecución de Dalvik para ejecutar código de apps. De forma predeterminada, Dalvik limita las apps a un único archivo de código de bytes classes.dex por APK. Para evitar este límite, puedes agregar la biblioteca de compatibilidad de multidex a tu proyecto de la siguiente manera:

dependencies {
    def multidex_version = "2.0.1"
    implementation 'androidx.multidex:multidex:$multidex_version'
}
   

Para ver las versiones actuales de esta biblioteca, consulta la información sobre Multidex en la página de versiones.

Si no estás utilizando AndroidX, agrega la siguiente dependencia de biblioteca de compatibilidad en su lugar:

dependencies {
  implementation 'com.android.support:multidex:1.0.3'
}

Esta biblioteca pasa a formar parte del archivo DEX principal de tu app y, luego, administra el acceso a los archivos DEX adicionales y al código que contienen. Puedes encontrar más detalles en la sección que aparece más abajo sobre cómo configurar tu app para multidex.

Compatibilidad de multidex con Android 5.0 y versiones posteriores

Android 5.0 (API nivel 21) y las versiones posteriores usan un tiempo de ejecución denominado ART, que admite de manera nativa la carga de varios archivos DEX desde archivos APK. Durante el tiempo de instalación de la app, ART lleva a cabo una compilación previa que realiza un escaneo en busca de archivos classesN.dex y los compila en un único archivo .oat para que se ejecute en el dispositivo Android. Por lo tanto, si la versión de minSdkVersion es 21 o una versión posterior, multidex estará habilitado de forma predeterminada y no se necesitará la biblioteca de compatibilidad multidex.

Para obtener más información sobre el tiempo de ejecución de Android 5.0, consulta ART y Dalvik.

Nota: Cuando se ejecuta la app con Android Studio, la compilación se optimiza para los dispositivos de destino en los que se implementará. Esto incluye la habilitación de multidex cuando los dispositivos de destino ejecutan Android 5.0 y versiones posteriores. Como esta optimización se aplica solo cuando se implementa la app con Android Studio, quizá de todos modos debas configurar tu compilación de lanzamiento para multidex si deseas evitar el límite de 64k.

Cómo evitar el límite de 64k

Antes de configurar tu app para habilitar el uso de 64k o más referencias de métodos, debes tomar medidas para reducir la cantidad total de referencias a las que llama el código de tu app, como los métodos definidos por el código de esta o las bibliotecas incluidas. Con las siguientes estrategias, puedes evitar alcanzar el límite de referencia de DEX:

  • Revisa las dependencias directas y transitivas de tu app: Asegúrate de que no se use ninguna dependencia de biblioteca importante que incluyas en tu app de una manera que implique superar el volumen de código agregado a la app. Un antipatrón común comprende la inclusión de una biblioteca muy grande porque unos pocos métodos de utilidades resultan provechosos. Reducir las dependencias de código de tu app suele ayudar a evitar el límite de referencia de DEX.
  • Quita el código que no se use con R8: Habilita la reducción de código a fin de ejecutar el R8 para las compilaciones de lanzamiento. Habilitar la reducción garantiza que no envíes código sin usar con tus APK.

Estas técnicas pueden ayudarte a evitar la necesidad de habilitar multidex en tu app y, al mismo tiempo, reducir el tamaño general de tu APK.

Cómo configurar tu app para multidex

Si minSdkVersion está configurado en 21 o una versión posterior, multidex estará habilitado de forma predeterminada, y no necesitarás la biblioteca de compatibilidad de multidex.

Sin embargo, si minSdkVersion está configurado en 20 o una versión anterior, deberás usar la biblioteca de compatibilidad de multidex y realizar las siguientes modificaciones en el proyecto de la app:

  1. Modifica el archivo build.gradle a nivel de módulo para habilitar multidex y agregar la biblioteca de multidex como dependencia, tal como se muestra a continuación:

    android {
        defaultConfig {
            ...
            minSdkVersion 15
            targetSdkVersion 28
            multiDexEnabled true
        }
        ...
    }
    
    dependencies {
      implementation 'com.android.support:multidex:1.0.3'
    }
    
  2. En función de si anulas la clase Application o no, realiza una de las siguientes acciones:
    • Si no anulas la clase Application, modifica tu archivo de manifiesto para definir android:name en la etiqueta <application>, como se muestra a continuación:

      <?xml version="1.0" encoding="utf-8"?>
      <manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.example.myapp">
          <application
                  android:name="android.support.multidex.MultiDexApplication" >
              ...
          </application>
      </manifest>
      
    • Si anulas la clase Application, debes cambiarla para extender MultiDexApplication (si es posible), como se muestra a continuación:

      Kotlin

      class MyApplication : MultiDexApplication() {...}
      

      Java

      public class MyApplication extends MultiDexApplication { ... }
      
    • También, si anulas la clase Application y no puedes cambiar la clase de base, como alternativa, puedes anular el método attachBaseContext() e invocar MultiDex.install(this) para habilitar multidex de la siguiente forma:

      Kotlin

      class MyApplication : SomeOtherApplication() {
      
          override fun attachBaseContext(base: Context) {
              super.attachBaseContext(base)
              MultiDex.install(this)
          }
      }
      

      Java

      public class MyApplication extends SomeOtherApplication {
        @Override
        protected void attachBaseContext(Context base) {
           super.attachBaseContext(base);
           MultiDex.install(this);
        }
      }
      

      Precaución: No ejecutes MultiDex.install() ni ningún otro código mediante reflexión o JNI antes de que se haya completado MultiDex.install(). El seguimiento de multidex no irá tras esas llamadas, lo que provocará errores de ClassNotFoundException o de verificación debido a una partición de clase incorrecta entre archivos DEX.

Cuando compilas tu app, las herramientas de compilación de Android crean un archivo DEX principal (classes.dex) y archivos DEX de respaldo (classes2.dex, classes3.dex, etc.) según sea necesario. El sistema de compilación luego empaqueta todos los archivos DEX en tu APK.

En el tiempo de ejecución, las API de multidex usan un cargador de clase especial para buscar tus métodos en todos los archivos DEX disponibles (en lugar de hacerlo solo en el archivo classes.dex principal).

Limitaciones de la biblioteca de compatibilidad de multidex

La biblioteca de compatibilidad de multidex tiene algunas limitaciones conocidas que debes tener en cuenta y para las cuales debes realizar pruebas cuando la incorpores en la configuración de compilación de tu app:

  • La instalación de archivos DEX durante el inicio en una partición de datos del dispositivo es compleja y puede generar errores de "Aplicación no responde" (ANR) si los archivos DEX secundarios son grandes. Si quieres evitar este problema, habilita la reducción de código para minimizar el tamaño de los archivos DEX y quitar las partes del código que no se usen.
  • Cuando ejecutas versiones anteriores a Android 5.0 (API nivel 21), el uso de multidex alcanza para evitar el límite linearalloc (problema 78035). Este límite se incrementó en Android 4.0 (API nivel 14), pero eso no resolvió por completo el problema. Y en las versiones anteriores a Android 4.0, es posible que alcances el límite de linearalloc antes de alcanzar el límite del índice DEX. Si orientas la app a niveles de API anteriores al 14, haz una prueba minuciosa con estas versiones de la plataforma, ya que tu app podría tener problemas durante el inicio o cuando se cargan grupos de clases particulares.

    La reducción de código puede disminuir o, quizás, eliminar esos problemas.

Cómo declarar las clases requeridas en el archivo DEX principal

Cuando compilas cada archivo DEX para una app multidex, las herramientas de compilación toman decisiones complejas a fin de determinar las clases que se necesitan en el archivo DEX principal de modo que la app pueda iniciarse con éxito. Si no se suministra alguna de las clases requeridas durante el inicio en el archivo DEX principal, tu app se bloqueará con el error java.lang.NoClassDefFoundError.

Esto no ocurrirá si accedes al código directamente desde el código de la app, ya que las herramientas de compilación reconocen las rutas de acceso de ese código. Sin embargo, puede suceder cuando las rutas de acceso del código son menos visibles; por ejemplo, cuando una de las bibliotecas usadas tiene dependencias complejas. Por ejemplo, si el código usa introspección o invocación de métodos Java del código nativo, es posible que esas clases no se consideren requeridas en el archivo DEX principal.

Entonces, si ves java.lang.NoClassDefFoundError, debes establecer esas clases como requeridas manualmente en el archivo DEX principal, para lo cual debes declararlas con las propiedades multiDexKeepFile o multiDexKeepProguard en tu tipo de compilación. Si se hace coincidir una clase, ya sea en el archivo de multiDexKeepFile o de multiDexKeepProguard, se agrega al archivo DEX principal.

Propiedad multiDexKeepFile

El archivo que especificas en multiDexKeepFile debe contener una clase por línea, en el formato com/example/MyClass.class. Por ejemplo, puedes crear un archivo denominado multidex-config.txt parecido al siguiente:

com/example/MyClass.class
com/example/MyOtherClass.class

Luego, puedes declarar ese archivo para un tipo de compilación, como se indica a continuación:

android {
    buildTypes {
        release {
            multiDexKeepFile file('multidex-config.txt')
            ...
        }
    }
}

Recuerda que Gradle lee las rutas de acceso relativas al archivo build.gradle, por lo cual, el ejemplo anterior funciona si multidex-config.txt está en el mismo directorio que el archivo build.gradle.

Propiedad multiDexKeepProguard

El archivo multiDexKeepProguard usa el mismo formato que ProGuard y admite toda su gramática. Para obtener más información sobre este tema, consulta la sección Opciones -keep del manual de ProGuard.

El archivo que especifiques en multiDexKeepProguard deberá incluir opciones -keep en cualquier sintaxis de ProGuard válida, por ejemplo, -keep com.example.MyClass.class. Puedes crear un archivo denominado multidex-config.pro parecido al siguiente:

-keep class com.example.MyClass
-keep class com.example.MyClassToo

Si deseas especificar todas las clases en un paquete, el archivo tendrá el siguiente aspecto:

-keep class com.example.** { *; } // All classes in the com.example package

Luego, puedes declarar ese archivo para un tipo de compilación, como se indica a continuación:

android {
    buildTypes {
        release {
            multiDexKeepProguard file('multidex-config.pro')
            ...
        }
    }
}

Cómo optimizar multidex en compilaciones de desarrollo

Una configuración de multidex requiere un tiempo de proceso de compilación mucho mayor, ya que el sistema de compilación debe tomar decisiones complejas sobre las clases que deben incluirse en el archivo DEX principal y las que pueden incluirse en los archivos DEX secundarios. Por este motivo, las compilaciones incrementales que usan multidex suelen tardar más tiempo y pueden ralentizar tu proceso de desarrollo.

Para mitigar los tiempos de compilación incrementales más largos, debes usar el proceso de pre-dexing a fin de reutilizar los resultados de multidex entre compilaciones. El proceso de pre-dexing se basa en un formato ART solo disponible en Android 5.0 (API nivel 21) y versiones posteriores. Si usas Android Studio 2.3 y versiones posteriores, el IDE utiliza automáticamente este elemento cuando implementa tu app en un dispositivo con Android 5.0 (API nivel 21) o versiones posteriores.

Sugerencia: La versión 3.0.0 y las versiones posteriores del complemento de Android para Gradle incluyen mejoras adicionales que optimizan las velocidades de compilación, como el proceso de dex por clase (de modo que solo las clases que modifiques se vuelvan a convertir a DEX). En general, para que tengas la mejor experiencia de desarrollo, siempre debes actualizar a la versión de Android Studio más reciente y a la versión más reciente del complemento de Android.

Sin embargo, si estás ejecutando compilaciones de Gradle desde la línea de comandos, debes establecer minSdkVersion en la versión 21 o en una versión posterior para habilitar el proceso de pre-dexing. Una estrategia útil para conservar las opciones de tu compilación de producción es crear dos versiones de la app usando variantes de productos: una variante de desarrollo y una de lanzamiento con valores diferentes para minSdkVersion, como se muestra a continuación:

android {
    defaultConfig {
        ...
        multiDexEnabled true
        // The default minimum API level you want to support.
        minSdkVersion 15
    }
    productFlavors {
        // Includes settings you want to keep only while developing your app.
        dev {
            // Enables pre-dexing for command line builds. When using
            // Android Studio 2.3 or higher, the IDE enables pre-dexing
            // when deploying your app to a device running Android 5.0
            // (API level 21) or higher—regardless of what you set for
            // minSdkVersion.
            minSdkVersion 21
        }
        prod {
            // If you've configured the defaultConfig block for the production version of
            // your app, you can leave this block empty and Gradle uses configurations in
            // the defaultConfig block instead. You still need to include this flavor.
            // Otherwise, all variants use the "dev" flavor configurations.
        }
    }
    buildTypes {
        release {
            minifyEnabled true
            proguardFiles getDefaultProguardFile('proguard-android.txt'),
                                                 'proguard-rules.pro'
        }
    }
}
dependencies {
    implementation 'com.android.support:multidex:1.0.3'
}

Para conocer más estrategias que ayudan a mejorar las velocidades de compilación (desde Android Studio o la línea de comandos), consulta Cómo optimizar la velocidad de compilación. Para obtener más información sobre el uso de variantes de compilación, consulta Cómo configurar variantes de compilación.

Sugerencia: Ahora que tienes distintas variantes de compilación para diferentes necesidades de multidex, también puedes proporcionar un archivo de manifiesto diferente para cada variante (de modo que solo la variante para la API nivel 20 y versiones anteriores cambie el nombre de la etiqueta <application>) o crear una subclase de Application diferente para cada variante (de modo que solo la variante para la API nivel 20 y versiones anteriores extienda la clase MultiDexApplication o llame a MultiDex.install(this)).

Cómo probar las apps de multidex

Cuando escribes pruebas de instrumentación para apps de multidex, no se requiere ninguna configuración adicional si usas una instrumentación MonitoringInstrumentation (o AndroidJUnitRunner). Si usas otra Instrumentation, debes anular su método onCreate() con el siguiente código:

Kotlin

fun onCreate(arguments: Bundle) {
  MultiDex.install(targetContext)
  super.onCreate(arguments)
  ...
}

Java

public void onCreate(Bundle arguments) {
  MultiDex.install(getTargetContext());
  super.onCreate(arguments);
  ...
}