为方法数超过 64K 的应用启用 multidex

如果您的应用的 minSdk 是 API 20 或更低版本,并且您的应用及其引用的库超过 65,536 个方法,您会遇到以下构建错误,这表明您的应用已达到 Android 构建架构的限制:

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

旧版本的构建系统会报告不同的错误,但指示的是相同的问题:

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

这些错误条件都显示一个共同的数字:65536。此数字表示单个 Dalvik Executable (DEX) 字节码文件中可被代码调用的引用总数。本页面解释了如何通过启用称为 multidex 的应用配置来解决此限制,该配置允许您的应用构建和读取多个 DEX 文件。

关于 64K 引用限制

Android 应用 (APK) 文件以 Dalvik 可执行文件 (DEX) 格式包含可执行字节码文件,这些文件包含用于运行应用的编译代码。Dalvik 可执行文件规范将单个 DEX 文件中可引用的方法总数限制为 65,536 个,其中包括 Android 框架方法、库方法和您自己代码中的方法。

在计算机科学中,术语 kilo(或 K)表示 1024 (或 2^10)。由于 65,536 等于 64x1024,因此此限制被称为 _64K 引用限制_。

Android 5.0 之前的 multidex 支持

Android 5.0(API 级别 21)之前的平台版本使用 Dalvik 运行时执行应用代码。默认情况下,Dalvik 将应用限制为每个 APK 一个 classes.dex 字节码文件。为了绕过此限制,请将 multidex 库添加到模块级 build.gradlebuild.gradle.kts 文件中:

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")
}

此库将成为您应用的主要 DEX 文件的一部分,然后管理对其他 DEX 文件及其包含的代码的访问。要查看此库的当前版本,请参阅 multidex 版本

如需了解更多详细信息,请参阅有关如何为 multidex 配置应用的部分。

Android 5.0 及更高版本的 multidex 支持

Android 5.0(API 级别 21)及更高版本使用名为 ART 的运行时,该运行时原生支持从 APK 文件加载多个 DEX 文件。ART 在应用安装时执行预编译,扫描 classesN.dex 文件并将它们编译成单个 OAT 文件,供 Android 设备执行。因此,如果您的 minSdkVersion 为 21 或更高,则 multidex 默认启用,您无需 multidex 库。

有关 Android 5.0 运行时的更多信息,请阅读 Android Runtime (ART) 和 Dalvik

注意:当您使用 Android Studio 运行应用时,构建会针对您部署到的目标设备进行优化。这包括当目标设备运行 Android 5.0 及更高版本时启用 multidex。由于此优化仅在您使用 Android Studio 部署应用时应用,因此您可能仍需要为 multidex 配置发布版本,以避免 64K 限制。

避免 64K 限制

在配置您的应用以启用使用 64K 或更多方法引用之前,请采取措施减少您的应用代码(包括您的应用代码或包含库定义的方法)调用的引用总数。

以下策略可以帮助您避免达到 DEX 引用限制:

审查应用的直接和传递依赖项
考虑您在应用中包含的任何大型库依赖项的价值是否超过了添加到应用中的代码量。一种常见但有问题的模式是,仅仅因为一些实用方法有用而包含一个非常大的库。减少应用代码依赖项通常可以帮助您避免 DEX 引用限制。
使用 R8 移除未使用代码
启用代码缩减以对发布版本运行 R8。启用缩减有助于确保您的 APK 不会发布未使用的代码。如果代码缩减配置正确,它还可以从您的依赖项中移除未使用的代码和资源。

使用这些技术可以帮助您减小 APK 的整体大小,并避免在应用中需要 multidex。

为 multidex 配置您的应用

注意:如果您的 minSdkVersion 设置为 21 或更高,则 multidex 默认启用,您无需 multidex 库。

如果您的 minSdkVersion 设置为 20 或更低,则您必须使用 multidex 库并对您的应用项目进行以下修改:

  1. 修改模块级 build.gradle 文件以启用 multidex 并将 multidex 库添加为依赖项,如下所示:

    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. 根据您是否覆盖 Application 类,执行以下操作之一:
    • 如果您不覆盖 Application 类,请编辑您的清单文件,将 <application> 标记中的 android:name 设置如下:

      <?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>
    • 如果您确实覆盖了 Application 类,请将其更改为扩展 MultiDexApplication,如下所示:

      Kotlin

      class MyApplication : MultiDexApplication() {...}

      Java

      public class MyApplication extends MultiDexApplication { ... }
    • 如果您覆盖了 Application 类,但无法更改基类,则可以改为覆盖 attachBaseContext() 方法并调用 MultiDex.install(this) 以启用 multidex:

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

      注意:MultiDex.install() 完成之前,不要通过反射或 JNI 执行 MultiDex.install() 或任何其他代码。Multidex 跟踪不会跟踪这些调用,从而导致 ClassNotFoundException 或由于 DEX 文件之间类分区错误而导致的验证错误。

现在,当您构建应用时,Android 构建工具会根据需要构建一个主要 DEX 文件 (classes.dex) 和支持性 DEX 文件 (classes2.dexclasses3.dex 等)。然后,构建系统会将所有 DEX 文件打包到您的 APK 中。

在运行时,multidex API 不仅在主要的 classes.dex 文件中搜索,还会使用特殊的类加载器在所有可用的 DEX 文件中搜索您的方法。

multidex 库的局限性

multidex 库存在一些已知限制。当您将该库集成到应用构建配置中时,请考虑以下事项:

  • DEX 文件在启动期间安装到设备的数据分区很复杂,如果辅助 DEX 文件很大,可能会导致应用无响应 (ANR) 错误。为避免此问题,请启用代码缩减以最小化 DEX 文件的大小并移除未使用的代码部分。
  • 在 Android 5.0(API 级别 21)之前的版本上运行时,使用 multidex 不足以解决 linearalloc 限制(问题 37008143)。此限制在 Android 4.0(API 级别 14)中有所增加,但并未完全解决问题。

    在 Android 4.0 以下的版本中,您可能会在达到 DEX 索引限制之前达到 linearalloc 限制。因此,如果您的目标 API 级别低于 14,请在这些平台版本上进行彻底测试,因为您的应用在启动时或加载特定类组时可能会出现问题。

    代码缩减可以减少或可能消除这些问题。

在主要 DEX 文件中声明所需的类

为 multidex 应用构建每个 DEX 文件时,构建工具会执行复杂的决策,以确定主要 DEX 文件中需要哪些类,以便您的应用成功启动。如果启动期间所需的任何类未在主要 DEX 文件中提供,则您的应用会因错误 java.lang.NoClassDefFoundError 而崩溃。

构建工具会识别直接从您的应用代码访问的代码路径。但是,当代码路径不那么明显时(例如,当您使用的库具有复杂的依赖项时),可能会出现此问题。例如,如果代码使用自省或从原生代码调用 Java 方法,则这些类可能不会被识别为主要 DEX 文件中必需的。

如果您收到 java.lang.NoClassDefFoundError,则必须通过在构建类型中使用 multiDexKeepProguard 属性声明它们来手动指定主要 DEX 文件中所需的额外类。如果某个类在 multiDexKeepProguard 文件中匹配,则该类将被添加到主要 DEX 文件中。

multiDexKeepProguard 属性

multiDexKeepProguard 文件使用与 ProGuard 相同的格式,并支持整个 ProGuard 语法。有关如何自定义应用中保留内容的更多信息,请参阅自定义要保留的代码

您在 multiDexKeepProguard 中指定的文件应包含任何有效的 ProGuard 语法中的 -keep 选项。例如,-keep com.example.MyClass.class。您可以创建一个名为 multidex-config.pro 的文件,其内容如下:

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

如果要指定包中的所有类,文件内容如下:

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

然后,您可以为构建类型声明该文件,如下所示:

Groovy

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

Kotlin

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

优化开发构建中的 multidex

multidex 配置需要显著增加构建处理时间,因为构建系统必须就哪些类必须包含在主要 DEX 文件中以及哪些类可以包含在辅助 DEX 文件中做出复杂的决策。这意味着使用 multidex 的增量构建通常需要更长时间,并可能减慢您的开发过程。

为了减少更长的增量构建时间,请使用预先 DEX 化在构建之间重用 multidex 输出。预先 DEX 化依赖于仅在 Android 5.0(API 级别 21)及更高版本上可用的 ART 格式。如果您使用的是 Android Studio,则 IDE 在将您的应用部署到运行 Android 5.0(API 级别 21)或更高版本的设备时会自动使用预先 DEX 化。但是,如果您从命令行运行 Gradle 构建,则需要将 minSdkVersion 设置为 21 或更高才能启用预先 DEX 化。

为了保留生产构建的设置,您可以使用产品变种创建两个版本的应用——一个用于开发变种,一个用于发布变种——具有不同的 minSdkVersion 值,如下所示:

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")
}

要了解更多有助于提高 Android Studio 或命令行构建速度的策略,请阅读优化构建速度。有关使用构建变种的更多信息,请参阅配置构建变种

提示:如果您有针对不同 multidex 需求的不同构建变种,则可以为每个变种提供不同的清单文件,这样只有针对 API 级别 20 及更低版本的文件才会更改 <application> 标记名称。您还可以为每个变种创建不同的 Application 子类,这样只有针对 API 级别 20 及更低版本的子类才扩展 MultiDexApplication 类或调用 MultiDex.install(this)

测试 multidex 应用

如果您使用 MonitoringInstrumentation AndroidJUnitRunner 仪器,则在为 multidex 应用编写仪器测试时不需要额外的配置。如果您使用其他 Instrumentation,则必须使用以下代码覆盖其 onCreate() 方法:

Kotlin

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

Java

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