为方法数超过 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 可执行 (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 运行时 (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 追踪将不会跟踪这些调用,从而导致由于 DEX 文件之间的类分区错误而导致 ClassNotFoundException 或验证错误。

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

在运行时,Multidex API 使用特殊的类加载器搜索所有可用的 DEX 文件中的方法,而不是仅搜索主 classes.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 限制。因此,如果您要定位低于 14 的 API 级别,请在这些平台版本上彻底测试,因为您的应用在启动时或加载特定组类时可能会出现问题。

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

声明主 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 应用

当您为 Multidex 应用编写 Instrumentation 测试时,如果您使用 MonitoringInstrumentation AndroidJUnitRunner Instrumentation,则不需要其他配置。如果您使用其他 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);
  ...
}