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

如果您的应用的 minSdk 为 API 20 或更低版本,并且您的应用及其引用的库包含的方法数超过 65,536,您会遇到以下构建错误,指明您的应用已达到 Android build 架构规定的上限:

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 Executable (DEX) 文件形式的可执行字节码文件,这些文件包含用来运行应用的已编译代码。Dalvik Executable 规范将可在单个 DEX 文件内引用的方法总数限制为 65,536,其中包括 Android 框架方法、库方法以及您自己的代码中的方法。

在计算机科学领域内,术语千(简称 K)表示 1024(即 2^10)。由于 65,536 等于 64X1024,因此这一限制称为“_64K reference limit_”。

Android 5.0 之前版本的 MultiDex 支持

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

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 运行时 (ART) 和 Dalvik

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

规避 64K 限制

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

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

查看应用的直接和传递依赖项
考虑您在应用中包含任何庞大的库依赖项所带来的价值是否多于为应用添加大量代码所带来的弊端。一种常见的问题模式是,仅仅为了使用几个实用方法就在应用中加入非常庞大的库。减少应用代码依赖项往往能够帮助您规避 DEX 引用限制。
通过 R8 移除未使用的代码
启用代码缩减以针对发布 build 运行 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 使用特殊的类加载器来搜索适用于您的方法的所有 DEX 文件,而不是只在主 classes.dex 文件中搜索。

对 MultiDex 库的限制

MultiDex 库具有一些已知的局限性。将该库纳入您的应用 build 配置时,请考虑以下方面:

  • 启动期间在设备的数据分区上安装 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,则必须使用 build 类型中的 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

然后,您可以针对 build 类型声明该文件,如下所示:

Groovy

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

Kotlin

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

在开发 build 中优化 MultiDex

MultiDex 配置会大幅增加构建处理时间,因为构建系统必须就哪些类必须包含在主要 DEX 文件中以及哪些类可以包含在辅助 DEX 文件中做出复杂的决策。这意味着,使用 MultiDex 的增量构建通常耗时较长,可能会拖慢您的开发进度。

如需缩短较长的增量构建时间,请使用 dex 预处理进程在 build 之间重复使用 MultiDex 输出。dex 预处理依赖于一种只在 Android 5.0(API 级别 21)及更高版本中提供的 ART 格式。如果您使用的是 Android Studio,那么在将您的应用部署到搭载 Android 5.0(API 级别 21)或更高版本的设备上时,IDE 会自动使用 dex 预处理进程。不过,如果您是从命令行运行 Gradle build,则需要将 minSdkVersion 设为 21 或更高版本以启用 dex 预处理。

如需保留正式版 build 的设置,您可使用产品变种创建应用的两个版本,其中一个版本具备开发变种,另一个版本具备发布变种,二者具有不同的 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 或命令行改进构建速度的策略,请阅读优化构建速度。如需详细了解如何使用 build 变体,请参阅配置 build 变体

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

测试 MultiDex 应用

编写面向 MultiDex 应用的插桩测试时,如果使用 MonitoringInstrumentationAndroidJUnitRunner 插桩,则不需要额外的配置。如果使用其他 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);
  ...
}