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

Si tu app tiene un minSdk de nivel de API 20 o inferior y tu app y las bibliotecas a las que hace referencia superan los 65,536 métodos, verás el siguiente error de compilación que indica que tu 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

Estas 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 64 K

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 esa limitación, agrega la biblioteca multidex al archivo build.gradle o build.gradle.kts a nivel del módulo:

Groovy

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

Kotlin

dependencies {
    val multidex_version = "2.0.1"
    implementation("androidx.multidex:multidex:$multidex_version")
}

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. Para ver las versiones actuales de esta biblioteca, consulta las versiones de multidex.

Si quieres obtener más detalles, consulta la sección para configurar tu app para multidex.

Compatibilidad de multidex con Android 5.0 y versiones posteriores

Android 5.0 (nivel de API 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 necesita la biblioteca multidex.

Para obtener más información sobre el tiempo de ejecución de Android 5.0, consulta Android Runtime (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, toma 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.

Las siguientes estrategias pueden ayudarte a evitar alcanzar el límite de referencia de DEX:

Revisa las dependencias directas y transitivas de tu app
Considera si el valor de cualquier dependencia de biblioteca importante que incluyas en tu app supera el volumen de código agregado a la app. Un patrón común pero problemático 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 para ejecutar el R8 para las compilaciones de lanzamiento. Esta reducción garantiza que no envíes código sin usar con tus APKs. Si la reducción de código se configura de forma correcta, también puede quitar el código y los recursos sin usar de tus dependencias.

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

Configura tu app para multidex

Nota: Si se configura minSdkVersion en 21 o un nivel superior, multidex está habilitado de forma predeterminada, y no necesitas la biblioteca correspondiente.

Si minSdkVersion está configurado en 20 o niveles inferiores, deberás usar la biblioteca multidex y realizar las siguientes modificaciones en el proyecto de la app:

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

    Groovy

    android {
        defaultConfig {
            ...
            minSdkVersion 15 
            targetSdkVersion 33
            multiDexEnabled true
        }
        ...
    }
    
    dependencies {
        implementation "androidx.multidex:multidex:2.0.1"
    }
    

    Kotlin

    android {
        defaultConfig {
            ...
            minSdk = 15 
            targetSdk = 33
            multiDexEnabled = true
        }
        ...
    }
    
    dependencies {
        implementation("androidx.multidex:multidex:2.0.1")
    }
    
  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="androidx.multidex.MultiDexApplication" >
              ...
          </application>
      </manifest>
      
    • Si anulas la clase Application, debes cambiarla para extender MultiDexApplication, como se muestra a continuación:

      Kotlin

      class MyApplication : MultiDexApplication() {...}
      

      Java

      public class MyApplication extends MultiDexApplication { ... }
      
    • Si anulas la clase Application, pero no es posible cambiar la clase base, como alternativa, anula el método attachBaseContext() e invoca 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 APIs 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 multidex

La biblioteca multidex tiene algunas limitaciones conocidas. Cuando incorpores la biblioteca en la configuración de compilación de tu app, ten en cuenta lo siguiente:

  • 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 (nivel de API 21), el uso de multidex alcanza para evitar el límite linearalloc (problema 37008143). Este límite se incrementó en Android 4.0 (nivel de API 14), pero eso no resolvió por completo el problema.

    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 inferiores 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.

Las herramientas de compilación reconocen las rutas de acceso del código al que se accede directamente desde el código de tu app. Sin embargo, este problema puede producirse cuando las rutas de acceso del código son menos visibles; por ejemplo, cuando una biblioteca que usas 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.

Si recibes java.lang.NoClassDefFoundError, debes especificar manualmente las clases adicionales requeridas en el archivo DEX principal, para lo cual debes declararlas con la propiedad multiDexKeepProguard en tu tipo de compilación. Si se hace coincidir una clase en el archivo de multiDexKeepProguard, se agrega al archivo DEX principal.

Propiedad multiDexKeepProguard

El archivo multiDexKeepProguard usa el mismo formato que ProGuard y admite toda su gramática. Si quieres obtener más información para personalizar lo que se guarda en tu app, consulta Cómo personalizar el código que se conservará.

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:

Groovy

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

Kotlin

android {
    buildTypes {
        getByName("release") {
            multiDexKeepProguard = file("multidex-config.pro")
            ...
        }
    }
}

Optimiza 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, usa el proceso de pre-dexing para reutilizar los resultados de multidex entre compilaciones. El proceso de pre-dexing se basa en un formato ART solo disponible en Android 5.0 (nivel de API 21) y versiones posteriores. Si usas Android Studio, el IDE utiliza automáticamente el proceso de pre-dexing cuando implementa tu app en un dispositivo con Android 5.0 (nivel de API 21) o versiones posteriores. Sin embargo, si ejecutas compilaciones de Gradle desde la línea de comandos, debes establecer minSdkVersion en el nivel de API 21 o en uno superior para habilitar el proceso de pre-dexing.

Para conservar la configuración de tu compilación de producción, puedes 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:

Groovy

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 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 "androidx.multidex:multidex:2.0.1"
}

Kotlin

android {
    defaultConfig {
        ...
        multiDexEnabled = true
        // The default minimum API level you want to support.
        minSdk = 15
    }
    productFlavors {
        // Includes settings you want to keep only while developing your app.
        create("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 minSdkVersion.
            minSdk = 21
        }
        create("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 {
        getByName("release") {
            isMinifyEnabled = true
            proguardFiles(getDefaultProguardFile("proguard-android.txt"),
                                                 "proguard-rules.pro")
        }
    }
}

dependencies {
    implementation("androidx.multidex:multidex:2.0.1")
}

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 el nivel de API 20 y niveles inferiores cambie el nombre de la etiqueta <application>). También puedes crear una subclase de Application distinta para cada variante (de modo que solo la variante para el nivel de API 20 y niveles inferiores extienda la clase MultiDexApplication o llame a MultiDex.install(this)).

Prueba 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);
  ...
}