Configure Apps with Over 64K Methods

原文链接

随着Android平台的不断成长,Android应用程序的数量也在不断增长。当你的应用和库的引用达到一个确定的大小,你会遇到应用达到Android应用构建架构限制的编译错误。早期版本的构建系统错误报告如下:

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

对于这个问题,近期的Android构建系统版本给出了一个不同的错误:

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

这两个错误情况都给出了一个相同的数字65,536。这个数字很重要,它表示一个DEX字节码文件可以被调用的引用总数。本文说明如何通过multidex解决此限制,让你的App可以构建和读取多Dex文件。

关于64k引用限制

APK文件中包含了一个可执行的DEX字节码文件,它包含了用于运行应用的编译代码。Dalvik Executable规范将单个DEX文件中可以引用的方法的总数限制为65,536,包括Android框架方法,库方法和你自己代码中的方法。在计算机科学的上下文中,术语Kilo,K表示1024(或2^10)。因为65,536等于64 X 1024,所以此限制称为“64K引用限制”。

Android5.0之前对Multidex的支持

在Android5.0(API level 21)之前使用Dalvik运行时执行应用程序。默认情况下,Dalvik将每个应用程序限制为一个classes.dex字节码文件。为了避免这个限制,你可以使用multidex support库,它作为主Dex文件的一部分可以管理对其他DEX文件及其包含的代码的访问。

注意: 如果你的项目配置为使用minSdkVersion 20或更低版本的multidex,并且运行在Android4.4(API 20)或更低版本的目标设备,Android Studio会禁用即时运行。

Android5.0及以上版本对Multidex的支持

Android5.0(APT 21)及以上版本使用了ART运行时,它本身支持从APK文件中加载多Dex文件。ART在App安装时执行预编译扫描classesN.dex文件并且把他们编译为一个可以在Android设备上执行的.oat文件。因此,如果你的minSdkVersion是21或更高,你就不需要multidex support库。

关于Android5.0运行时的更多信息,请阅读ART和Dalvik

注意: 当使用即时运行,并且你的应用的minSdkVersion是21或更高,Android Studio会自动配置你的App为multidex。因为即时运行只会在App的debug版本工作,因此为了避免64k限制的问题你依然需要为发布版本配置multidex。

避免64k限制

在配置应用以启用使用64K或更多方法引用之前,你应该采取措施减少应用代码调用的引用总数,包括应用代码以及包含的库定义的方法。以下策略可以帮助你避免达到DEX引用限制:

  • 检查应用程序的直接和间接依赖 - 确保你的应用中任何大型的依赖库的使用大于添加到应用程序的代码量。一个常见的范模式是由于要使用少量的使用方法而添加了一个大型的依赖库。减少你的应用代码依赖库可以帮助避免DEX引用限制。
  • Remove unused code with ProGuard - Enable code shrinking to run ProGuard for your release builds. Enabling shrinking ensures you are not shipping unused code with your APKs.
  • 使用ProGuard移除未使用的代码 - 启用代码shrinking以针对你的发布版本运行ProGuard,以确保你不会在APK中添加未使用的代码。

使用这些技术可能会帮助你避免在应用程序中启用multidex,同时还会减少APK的大小。

给你的App配置Multidex

给你的应用程序配置Multidex,需要根据你的应用程序可以支持的最低版本(minSdkVersion)对项目做以下修改。

如果minSdkVersion是21或更高,你只需要在module级别的build.gradle文件中设置multiDexEnabledtrue,像这样:

1
2
3
4
5
6
7
8
9
android {
defaultConfig {
...
minSdkVersion 21
targetSdkVersion 25
multiDexEnabled true
}
...
}

然而,如果你的minSdkVersion是20或更低,你就必须使用multidex support library,像这样:

  • 修改module级别的build.gradle文件,设置multiDexEnabledtrue,并且添加multidex库依赖:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    android {
    defaultConfig {
    ...
    minSdkVersion 15
    targetSdkVersion 25
    multiDexEnabled true
    }
    ...
    }

    dependencies {
    compile 'com.android.support:multidex:1.0.1'
    }
  • 根据你是否重写了Application类,做如下相应的改动:

    • 如果你没有重写Application类,将manifest文件中的<application> 标签的android:name改为如下:

      1
      2
      3
      4
      5
      6
      7
      8
      <?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>
    • 如果你重写了Application类,并且Application可以继承MultiDexApplication,改为如下:

      1
      public class MyApplication extends MultiDexApplication { ... }
    • 如果你重写了Application类,并且Application无法继承MultiDexApplication,你就需要重写Application的attachBaseContext()方法去调用MultiDex.install(this)来启用multidex:

      1
      2
      3
      4
      5
      6
      7
      public class MyApplication extends SomeOtherApplication {
      @Override
      protected void attachBaseContext(Context base) {
      super.attachBaseContext(context);
      Multidex.install(this);
      }
      }

现在当你构建你的App,Android构建工具会根据需求构建主Dex文件(classes.dex)和支持Dex文件(classes2.dex,calsses2.dex,…)。然后构建系统会将所有的Dex文件打包进APK。

在运行时,对于一个方法multidex API使用特殊的类加载器来搜索所有可用的DEX文件(而不是仅在主classes.dex文件中搜索)。

Multidex Support库的限制

Multidex Support库有一些已知的限制,当你的应用程序设置了Multidex的时候你应该注意和测试:

  • 在启动过程中将DEX文件安装到设备的数据分区上很复杂,如果辅助DEX文件很大,可能导致应用程序无响应(ANR)错误。在这种情况下,你应该使用ProGuard缩减应用代码以最小化DEX文件的大小并删除未使用的代码部分。
  • 由于Dalvik linearAlloc错误(问题22586),使用multidex的应用程序可能无法在运行低于Android 4.0(API 14)的平台的设备上启动。如果你的API级别低于14,请务必使用这些版本的平台进行测试,因为你的应用在启动或加载特定类别的组时可能会遇到问题。代码缩减可以减少或可能消除这些潜在问题。
  • 由于Dalvik linearAlloc限制(问题78035),使用multidex配置的应用程序会产生非常大的内存分配请求,因此可能会在运行时崩溃。Android 4.0(API 14)中的分配限制已增加,但在Android 5.0(API 21)之前的Android版本中,应用仍可能达到此限制。

声明主Dex文件中所需的类

当为一个multidex应用程序构建每个DEX文件时,构建工具执行着复杂的决策,以确定在主DEX文件中需要哪些类,以便你的应用程序可以成功启动。如果在主DEX文件中没有提供启动期间所需的类,那么你的应用程序会崩溃,并返回错误java.lang.NoClassDefFoundError

这不应该发生在直接从应用程序访问的代码上,因为构建工具会识别这些代码路径,但是当代码路径不太明显时,例如当ni 使用的库具有复杂的依赖关系时,就会发生这种情况。比如,如果代码使用反射或从native代码调用Java方法,那么这些类可能无法在主DEX文件中识别为必需。

因此,如果你看到java.lang.NoClassDefFoundError,就必须在主DEX文件中通过使用构建类型中的multiDexKeepFilemultiDexKeepProguard属性声明这些附加类,从而手动指定这些附加类。如果一个类在multiDexKeepFilemultiDexKeepProguard文件中匹配,那么该类将被添加到主DEX文件中。

multiDexKeepFile属性

声明在multiDexKeepFile中的类应该一行一个类,格式是com/example/MyClass.class。比如,你可以创建一个multidex-config.txt文件如下:

1
2
com/example/MyClass.class
com/example/MyOtherClass.class

接着,你可以将这个文件声明为一个构建类型:

1
2
3
4
5
6
7
8
android {
buildTypes {
release {
multiDexKeepFile file 'multidex-config.txt'
...
}
}
}

记住,Gradle读取相对于build.gradle文件的路径,因此如果multidex-config.txtbuild.gradle文件在同一目录中,则上述示例工作。

multiDexKeepProguard属性

multiDexKeepProguard文件和Proguard使用相同的格式,并且支持所有的Proguard语法。关于Proguard的格式和语法的更多信息,可以查看Proguard手册中的保留选项部分。

The file you specify in multiDexKeepProguard should contain -keep options in any valid ProGuard syntax. For example, -keep com.example.MyClass.class. You can create a file called multidex-config.pro that looks like this:

你在multiDexKeepProguard中制定的任何有效的ProGuard语法都应该包含-keep选项。比如,-keep com.example.MyClass.class。你可以创建一个叫做multidex-config.pro的文件,像这样:

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

如果你想制定一个包中的所有类,可以像这样:

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

接着,你可以将这个文件声明为一个构建类型:

1
2
3
4
5
6
7
8
android {
buildTypes {
release {
multiDexKeepProguard 'multidex-config.pro'
...
}
}
}

在开发构建中优化multidex

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

要减少生产multidex的较长构建时间,可以使用productFlavors创建两个构建类型:一个用于开发,一个用于发布,它们有不同的minSdkVersion值。

For the development flavor, set minSdkVersion to 21. This setting enables a build feature called pre-dexing, which generates multidex output much faster using an ART format available only on Android 5.0 (API level 21) and higher. For the release flavor, set the minSdkVersion as appropriate for your actual minimum support level. This setting generates a multidex APK that is compatible with more devices, but takes longer to build.

对于开发版本,将minSdkVersion设置为21。此设置启用pre-dexing构建特性,仅适用于Android 5.0(API 21)或更高版本的ART格式,可以更快地生成multidex。 对于发行版本,将minSdkVersion设置为实际的最低支持版本。此设置会生成与更多设备兼容的multidex APK,但需要更长时间才能生成。

以下构建配置示例说明如何在Gradle文件中设置这些类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
android {
defaultConfig {
...
multiDexEnabled true
}
productFlavors {
dev {
// Enable pre-dexing to produce an APK that can be tested on
// Android 5.0+ without the time-consuming DEX build processes.
minSdkVersion 21
}
prod {
// The actual minSdkVersion for the production version.
minSdkVersion 14
}
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
}
dependencies {
compile 'com.android.support:multidex:1.0.1'
}

做了这个配置更改后,可以使用应用程序的devDebug变量进行增量构建,其中结合了开发产品类型和调试构建类型的属性。 这样可以创建一个可调试的应用程序,启用multidex并禁用proguard(因为默认情况下minifyEnabled为false)。这些设置会让Gradle的Android插件执行以下操作:

  1. 执行pre-dexing: 将每个应用程序模块和每个依赖项构建为单独的DEX文件。
  2. 在APK中包含每个DEX文件,而不进行修改(没有缩减代码)。
  3. 最重要的是,模块DEX文件不组合,因此避免了长时间运行计算,以确定主DEX文件的内容。

这些设置可以实现快速增量构建,因为在后续构建期间只需重新计算并重新打包修改模块的DEX文件。但是,这些版本的APK只能用于Android 5.0设备上测试。但是,通过将配置设置为一种版本,你可以保留使用适当的最低API级别和ProGuard做代码压缩,保留执行正常构建的能力。

还可以构建其它版本,比如一个prodDebug版本的构建,这需要更长的时间来构建,但可以用于开发之外的测试。在所示的配置中,prodRelease版本将是最后的测试和发布版本。有关使用构建版本的详细信息,请参阅配置构建

建议: 现在你对不同的multidex需求有不同的构建版本,你也可以为每个版本提供一个不同的Manifest文件(所以只有一个API级别20和更低版本的Manifest文件需要更改标签),或创建一个不同的Application子类(因此只要针对API级别20和更低版本扩展MultiDexApplication类或调用MultiDex.install(this))。

测试Multidex App

为multidex应用程序编写测试时,不需要其他配置。AndroidJUnitRunner支持multidex,只要在自定义Application对象中使用MultiDexApplication或重写attachBaseContext()方法,并调用MultiDex.install()this)即可启用multidex。

或者,你可以重写AndroidJUnitRunner中的onCreate()方法:

1
2
3
4
5
public void onCreate(Bundle arguments) {
MultiDex.install(getTargetContext());
super.onCreate(arguments);
...
}

注意: 目前不支持使用multidex创建测试APK。