如何配置方法数超过64K的应用
随着Android平台的继续成长,Android应用的大小也在变大。当一个应用及其引用的库到达一定的规模,在编译应用时就会遇到构建错误,这表示此App已经达到了Android构建系统的某个限制。在早期的构建系统版本中,此错误表现如下:
Conversion to Dalvik format failed:
Unable to execute dex: method ID not in [0, 0xffff]: 65536
现在的Android构建系统可能会报另一个不同的错误,但它们表示的都是同一个问题:
trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.
这些错误的发生条件都指向了一个共同的数字:65536,这个数字非常重要,它代表了一个单独的DEX字节码文件可执行的引用总数。本文将说明如何通过将一个应用配置为multidex的方式来解决该问题,配置之后,该应用将构建和读取多个DEX文件。
关于64K的引用限制
Android 应用(APK)中以Dalvik虚拟机可执行文件(即DEX)的形式包含了可执行的字节码文件,DEX文件中编译后的代码则用于运行App功能。DEX规范做了如下限制:一个单独的DEX文件可引用的最多方法数为65636——包括Android框架中的方法、第三方库的方法以及自己代码中的方法。在计算机科学的表述中,K表示1024(或2^10)。因为65536即64x1024,所以65536的方法数限制也叫做64K引用限制。
Android 5.0之前的版本使用Mutidex
Android 5.0(API level 21)之前的版本使用Dalvik虚拟机来执行应用代码,默认情况下,Dalvik限制每个APK中只有一个classes.dex字节码文件。为了绕过这个限制,可以使用支持库multidex support library,它将作为主DEX的一部分,用来管理对额外的DEX文件及其代码的访问。
说明:如果为minSdkVersion=20或更低的应用配置multidex,并且运行目标设备为Android 4.4(API level 20)或更低,此时Android Studio将禁止使用Instant Run。
Android 5.0及之后的版本使用Multidex
Android 5.0 (API level 21)及更高版本使用ART 运行时,ART默认支持从APK文件中加载多个DEX文件。当应用安装时,ART将执行预编译,会扫描多个.dex文件,并将它们编译为一个单独的.oat文件用来被Android设备执行。因此如果minSdkVersion=21或者更高,则不再需要multidex支持库。
关于更多的Anroid 5.0运行时知识,请参考ART and Dalvik。
说明:当使用Instant Run时,如果应用的minSdkVersion=21或者更高,则Android Studio会自动为该应用配置multidex。由于Instant Run仅用于App编译的debug模式,因此仍然需要为release模式配置multidex来避免64K的限制。
避免64K 方法数的限制
在为App配置允许使用64K或更多的方法引用之前,我们应该采取措施来减少App中所需的方法总数,包括我们自定义的方法及引用库的方法。以下策略可以帮助我们避免触发DEX的64K限制:
Review应用的直接依赖或传递依赖
Ensure any large library dependency you include in your app is used in a manner that outweighs the amount of code being added to the app. A common anti-pattern is to include a very large library because a few utility methods were useful. 减少应用的代码依赖可以有效地避免DEX引用限制。
使用ProGuard移除无用代码
为应用的release版本开启Proguard的Enable code shrinking,代码压缩将不会把无用代码打包到APK中。
使用上述方法可以帮助我们避免在应用中使用multidex,同时能够有效减少APK包的大小。
为应用配置multidex
为应用配置multidex需要对应用工程作一些修改,这取决于该App所支持的最低Android版本。
如果minSdkVersion
设置为21或更高,那么只需要为module中的build.gradle
文件添加multiDexEnabled
为true
,具体如下:
android {
defaultConfig {
...
minSdkVersion 21
targetSdkVersion 25
multiDexEnabled true
}
...
}
如果minSdkVersion
为20或更低,则必须使用multidex support library
,具体操作如下:
为Module中的
build.gradle
文件添加multiDexEnabled
为true
,并且添加multidex library依赖,如下所示:android { defaultConfig { ... minSdkVersion 15 targetSdkVersion 25 multiDexEnabled true } ... } dependencies { compile 'com.android.support:multidex:1.0.1' }
根据是否重写了
Application
类,选择下面的对应方案:如果没有重写
Application
类,将AndroidManifest.xml中的标签的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="android.support.multidex.MultiDexApplication" > ... </application> </manifest>
如果重写了
Application
类,让它继承MultiDexApplication
(如果可能):public class MyApplication extends MultiDexApplication { ... }
如果重写了
Application
类,但是它的基类无法修改,那么可以重写attachBaseContext()
方法并且调用MultiDex.install(this)
来允许使用multidex。public class MyApplication extends SomeOtherApplication { @Override protected void attachBaseContext(Context base) { super.attachBaseContext(context); Multidex.install(this); } }
现在当编译应用,Android构建工具将会构造一个主DEX文件(classes.dex),如果有需要的话还会生成其他DEX文件(如classes2.dex,classes3.dex等),构建系统然后会将所有的DEX文件打包到APK中。
在运行的时候,multidex API会使用特定的类加载器在所有可用的DEX文件中来搜索所需方法。
multidex support library的局限性
multidex support library
有一些已知的局限性,在为应用配置multidex之前我们需要知道并测试:
- 在启动时将DEX安装到设备的数据分区很复杂,如果第二个DEX文件很大的话,有可能导致ANR错误。这种情况下,我们需要开启code shrinking with ProGuard来减小DEX的大小并移除无用代码。
- 一些使用了multidex的应用可能无法在低于Android 4.0(API level 14)的设备上运行,这可能是由于
Dalvik linarAlloc bug
( Issue 22586)引起。如果目标API的版本低于14,必须确保对这些平台版本展开充分测试,因为它们可能在启动或某些特殊类加载时产生异常。Code shrinking可以减少或消除这些潜在的问题。 - 使用了multidex的应用需要更多的内存分配,也可能导致应用崩溃,这是由于
Dalvik linearAlloc limit
(Issue 78053)的原因。虽然内存分配限制在Android 4.0(API level 14)增加了,但App仍有可能在Android 5.0(API level 21)之前的版本中运行时到达上限。
为主DEX 文件声明必须类
为配置了multidex的应用构建每一个DEX文件时,构建工具需要做一个复杂的决策,来决定哪一个类是主DEX文件必须的,来保证应用能成功启动。如果某个在启动时必须的类没有被包含到主DEX文件,那么应用启动时就会崩溃,并报错java.lang.NoClassDefFoundError
。
如果我们应用的代码可以直接访问某些代码,上述错误一般来说不会发生,因为构建工具可以识别出这些代码路径,但是当代码路径不可见时就可能会发生上述异常,如应用中的某个库包含了复杂的依赖。一个更具体的例子,如代码使用了反射或从Native代码调用Java方法,这些方法则可能不会被识别并放到主DEX文件中。
所以当遇到java.lang.NoClassDefFoundError
时,需要手动指定哪些类为主DEX必须的,具体方法为通过在build type中使用multiDexKeepFile
属性来声明它们。在这里指定的文件必须每个类声明为一行,书写格式为com/example/MyClass.class
。例如,可以创建如下一个叫做dex.keep的文件:
com/example/MyClass.class
com/example/MyOtherClass.class
然后就可以为某个build type指定此文件:
android {
buildTypes {
release {
multiDexKeepFile file('dex.keep')
...
}
}
}
因为Gradle是相对于build.gradle文件读取路径的,所以上述例子中的dex.keep需要跟build.gradle文件处于相同的目录才可生效。
在开发中优化multidex的构建过程
配置了Multidex的应用所需要的构建时间明显增加,因为构建系统需要做复杂的分析来决定哪些类需要包含在主DEX中,哪些类需要包含则其他DEX文件中。这意味着使用multidex的构建会花费更多时间并减慢我们的开发过程。
为了缩短multidex的构建时间,我们可以使用productFlavors
创建两个构建变种:一个development flavor
和一个release flavor
,它们使用不同的minSdkVersion。
对于development flavor
,将minSdkVersion
设为21,此设置将会使用一种叫做per-dexing
的构建功能,在使用ART模式时(Android 5.0或更高),它会更快地为multidex生成APK。对于release flavor
,将minSdkVersion
设置为应用真正要支持的最低版本,这种设置方式将会生成兼容更多设备的APK,但是需要花费更长的构建时间。
下面的构建配置说明了在Gradle中如何设置上述flavor:
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
变种来进行增量构建,此种方式组合了dev
product flavor以及debug
构建类型。这将生成一个debug版的使用了multidex并且禁用proguard(因为minifyEnabled
默认为false)的应用。上述配置会让Gradle插件做以下事情:
- 执行
pre-dexing
:将每一个Module及每一个依赖编译成一个独立的DEX文件。 - 将每一个DEX文件不做任何修改地包含进APK文件中(没有执行代码压缩)。
- 最重要的是,每个Module生成的DEX没有组合起来,因此不需要花费很长时间来计算主DEX应该包含哪些内容。
上述配置会使得构建加快,因为仅仅是发生修改的module会被重计算及打包构建出新的DEX文件,但是,上述配置生成的APK只能在Android 5.0的设备上测试。然而通过使用flavor配置,我们也可以使用通用的构建方式来生成APK,这些APK可以适配最低的SDK版本并且使用了ProGuard
代码压缩。
还可以构建其他变种,如prodDebug
,它会花费较长时间来构建,但是可以用来做开发之外的测试。prodRelease
可作为最终的测试及发布版本。如需了解更多的构建变种知识,请参考 Configure Build Variants。
测试multidex应用
为multidex应用写测试用例时,无需其他配置。AndroidJUnitRunner
支持multidex,只要你使用MultiDexApplication
或者为自定义的Application
重写attachBaseContext()
方法并调用MultiDex.install(this)
来开启muxtidex支持。
另外,可以在AndroidJUnitRunner
中重写onCreate()
方法:
public void onCreate(Bundle arguments) {
MultiDex.install(getTargetContext());
super.onCreate(arguments);
...
}
说明:使用multidex来创建一个测试APK目前是不支持的。