写在前面
前几天,开发中遇到一个问题,Log信息如下:
E/AndroidRuntime(10943): FATAL EXCEPTION: main
E/AndroidRuntime(10943): Process: com.freeme.gallery, PID: 10943
E/AndroidRuntime(10943): java.lang.NoClassDefFoundError: com.freeme.gallery.data.DataManager$DateTakenComparator
E/AndroidRuntime(10943): at com.freeme.gallery.data.DataManager.<clinit>(DataManager.java:65)
E/AndroidRuntime(10943): at com.freeme.gallery.app.GalleryAppImpl.getDataManager(GalleryAppImpl.java:77)
E/AndroidRuntime(10943): at com.freeme.gallery.provider.GalleryProvider.onCreate(GalleryProvider.java:101)
E/AndroidRuntime(10943): at android.content.ContentProvider.attachInfo(ContentProvider.java:1656)
E/AndroidRuntime(10943): at android.content.ContentProvider.attachInfo(ContentProvider.java:1627)
E/AndroidRuntime(10943): at android.app.ActivityThread.installProvider(ActivityThread.java:5060)
E/AndroidRuntime(10943): at android.app.ActivityThread.installContentProviders(ActivityThread.java:4634)
E/AndroidRuntime(10943): at android.app.ActivityThread.handleBindApplication(ActivityThread.java:4567)
E/AndroidRuntime(10943): at android.app.ActivityThread.access$1500(ActivityThread.java:153)
E/AndroidRuntime(10943): at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1404)
E/AndroidRuntime(10943): at android.os.Handler.dispatchMessage(Handler.java:110)
E/AndroidRuntime(10943): at android.os.Looper.loop(Looper.java:193)
E/AndroidRuntime(10943): at android.app.ActivityThread.main(ActivityThread.java:5351)
E/AndroidRuntime(10943): at java.lang.reflect.Method.invokeNative(Native Method)
E/AndroidRuntime(10943): at java.lang.reflect.Method.invoke(Method.java:515)
E/AndroidRuntime(10943): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:835)
E/AndroidRuntime(10943): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:651)
E/AndroidRuntime(10943): at dalvik.system.NativeStart.main(Native Method)
从报错信息来看,是没有找到DateTakenComparator
这个内部类且又是运行时异常,那是不是和ClassLoader有关系呢?
那么首先排除代码原因,开始从Gradle和Gradle插件版本入手,通过改变版本来验证。然而验证下来发现与Gradle并没关系。
那么问题到底出在哪呢?
没辙!于是开始按节点排查,排查过几个关键节点后,终于得出一个结论:引入某个特定library后就会报这个错。
然而这个library是直接从Maven导入的,library本身肯定没有问题。似乎到这里线索又断了…恰逢此时,同事建议看下apk包大小。不看不知道,看过才恍然大悟,apk内大有乾坤啊。
apk包中含有两个.dex
文件:classes.dex
和classes2.dex
,再看java.lang.NoClassDefFoundError
,结果显而易见,方法数超限了!但是已经在build.gradle中配置了multiDexEnabled true
和添加了android.support.multidex
,为何还会出错呢? 原来是忘了继承MultiDexApplication
了!敲脑袋ing…
接下来,我们借助官方文档来了解下64K方法数限制。
正文
随着应用不断增加新功能,引入新库,apk会越来越大,到达一定规模后就可能遇到方法数超限问题。
早期版本错误信息如下:
Conversion to Dalvik format failed:
Unable to execute dex: method ID not in [0, 0xffff]: 65536
较新版本错误信息如下:
trouble writing output:
Too many field references: 131000; max is 65536.
You may try using --multi-dex option.
其中数字65536
是关键,Android平台的Java虚拟机Dalvik执行Dex程序时,使用的是short类型来索引DEX文件中的方法。这就意味着单个Dex文件可被引用的方法总数被限制为64x1024
, 即65536
。其中包括:
- Android Framework的方法
- library的方法
- 我们自己写的方法
为突破这个限制,需要使用multidex来生成多个dex文件。
Android5.0 (API level 21)之前版本支持Multidex
Android5.0之前使用Dalvik运行时执行应用代码,默认Dalvik限制每个apk只能有一个字节码classed.dex
文件。为突破这个限制,可以使用multidex support library来管理额外的dex文件(包括代码)。
Android5.0及更高版本支持Multidex
Android5.0及更高版本使用支持从apk中加载多个dex文件的ART运行时机制,在应用安装时,加载classed(…N).dex文件并编译成一个.oat
文件以支持在Android设备上运行。关于Android 5.0运行时详见ART介绍。
Note: While using Instant Run, Android Studio automatically configures your app for multidex when your app’s minSdkVersion is set to 21 or higher. Because Instant Run only works with the debug version of your app, you still need to configure your release build for multidex to avoid the 64K limit.
如果使用Instant Run,当app的minSdkVersion
大于或等于21
时,Android Studio会自动配置支持multidex,但是仅debug
版本有效,release
版仍然需要配置multidex来突破64K限制。
避免64K限制
在配置multidex之前,你或许可以通过以下方法来减小方法总数(包括引用的、library里的和自己写的方法)。
- 排除未使用的依赖 -此步骤通常能有效避免64K限制。
- 使用ProGuard去除未使用的方法 -为release版本配置ProGuard,能有效排除一些无用方法
使用以上技术能有效避免更改构建配置来引用更多的方法,同时能减小apk大小,使用户消耗更少的流量。
使用Gradle配置Multidex
Android SDK Build Tools 21.1或更高版本上支持multidex,确定要配置multidex前请确保Android SDK Build Tools
和Android Support Repository
更新到较新版本。
通过以下步骤配置multidex:
- 更改Gradle配置来支持multidex
- 修改manifest。使其支持multidexapplication类
修改模块级builde.gradle文件,修改如下:
android {
compileSdkVersion 21
buildToolsVersion "21.1.0"
defaultConfig {
...
minSdkVersion 14
targetSdkVersion 21
...
// Enabling multidex support.
multiDexEnabled true
}
...
}
dependencies {
compile 'com.android.support:multidex:1.0.0'
}
在manifest文件中,添加MultidexApplication Class的引用,如下:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.android.multidex.myapplication">
<application
...
android:name="android.support.multidex.MultiDexApplication">
...
</application>
</manifest>
通过以上步骤即可支持multidex。
Note: If your app uses extends the Application class, you can override the attachBaseContext() method and call MultiDex.install(this) to enable multidex. For more information, see the MultiDexApplication reference documentation.
如果你的应用中已经继承Application,那么可以通过复写attachBaseContext()
方法并调用MultiDex.install(this)
来支持multidex,即无需修改manifest文件。更多信息请看MultiDexApplication
补充:
亦可直接将继承Application
改为继承MultiDexApplication
,而无需修改manifest文件或复写attachBaseContext()
方法。
multidex support library的使用限制
multidex support library
有一些已知的限制请务必知晓,需要在应用时先行测试。
如果classes2.dex文件较大,安装dex文件到设备的数据区是一个复杂的过程,可能会导致应用程序无响应(ANR)的错误。在这种情况下,应该使用ProGuard尽量减小dex文件的大小且删除无用的代码。
在Android 4.0(API Level 14)之前,由于Dalvik linearalloc bug(问题22586),multidex可能是运行失败。如果希望运行在Level 14之前的Android系统版本,请先确保完整的测试和使用。优化代码可以减少或可能消除这些潜在的问题。
应用程序使用了multiedex配置,会造成申请很大的内存分配。可能还会引起Dalvik虚拟机的崩溃(问题78035)。此分配限制是在Android 4.0 (API level 14)上增加的,但Android5.0 (API level 21)之前的版本仍有此限制。
multidex构建工具不支持指定哪些类必须包含在首个dex文件中,因而可能导致某些library无法使用。
优化Multidex的开发和构建
multidex会加长构建应用的时间,这个必要的过程可能会拖慢你的开发进度。
为加速构建过程,我们可以在Gradle中配置productFlavors: a development flavor and a production flavor
.
开发时将minSdkVersion
改为21
使用ART运行时机制,这样能加快构建速度。release时改为合适的minSdkVersion
,这样仅在release时费时较长。
build.gradle配置如下:
android {
productFlavors {
// Define separate dev and prod product flavors.
dev {
// dev utilizes minSDKVersion = 21 to allow the Android gradle plugin
// to pre-dex each module and produce an APK that can be tested on
// Android Lollipop without time consuming dex merging processes.
minSdkVersion 21
}
prod {
// The actual minSdkVersion for the application.
minSdkVersion 14
}
}
...
buildTypes {
release {
runProguard true
proguardFiles getDefaultProguardFile('proguard-android.txt'),
'proguard-rules.pro'
}
}
}
dependencies {
compile 'com.android.support:multidex:1.0.0'
}
完成上述配置后,你可以使用结合了dev productFlavor
和buildType
属性的devDebug
变体app。
这个变体app包含如下特性:
- 关闭了混淆(proguard)
- 支持multidex
- minSdkVersion 设置为 Android API level 21.
这些设置将使Gradle插件做如下事情:
- 编译应用的每个模块(包括依赖)为独立的dex文件,这个过程称为
pre-dexing
- 不作修改地include每个dex文件到apk里
- 更重要的是,这些模块dex文件将不会合并,这样避免分割主dex文件,以加快速度
值得注意的是:上述配置后的devDebug变种app仅能运行在Android 5.0设备上。
同时,你也可以构建其他变体app,也可以在终端使用gradel命令来实现多渠道打包等。更多有关flavors
和Gradle tasks
信息, 请看Gradle Plugin User Guide(中文翻译).
在Android Studio中构建变种App
使用multidex时,构建变体app对管理构建过程是非常有用的。Android studio允许用户自己选择。
在Android Studio中构建变体app,步骤如下:
从左边栏打开
Build Variants
窗口点击
build variant
以选择不同变体,如图:
测试Multidex应用
测试multidex应用,需在build.gradle
中配置MultiDexTestRunner:
android {
defaultConfig {
...
testInstrumentationRunner "com.android.test.runner.MultiDexTestRunner"
}
}
Note: With Android Plugin for Gradle versions lower than 1.1, you need to add the following dependency for multidex-instrumentation:
若Gradle插件版本低于1.1,你还需添加multidex-instrumentation
依赖:
dependencies {
androidTestCompile('com.android.support:multidex-instrumentation:1.0.1') {
exclude group: 'com.android.support', module: 'multidex'
}
}
备注:文中链接为官方链接,请爬墙观看!