寫在前面
前幾天,開發(fā)中遇到一個問題,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這個內(nèi)部類且又是運行時異常,那是不是和ClassLoader有關(guān)系呢?
那么首先排除代碼原因,開始從Gradle和Gradle插件版本入手,通過改變版本來驗證。然而驗證下來發(fā)現(xiàn)與Gradle并沒關(guān)系。
那么問題到底出在哪呢?
沒轍!于是開始按節(jié)點排查,排查過幾個關(guān)鍵節(jié)點后,終于得出一個結(jié)論:引入某個特定library后就會報這個錯。
然而這個library是直接從Maven導(dǎo)入的,library本身肯定沒有問題。似乎到這里線索又斷了...恰逢此時,同事建議看下apk包大小。不看不知道,看過才恍然大悟,apk內(nèi)大有乾坤啊。
apk包中含有兩個.dex文件:classes.dex和classes2.dex,再看java.lang.NoClassDefFoundError,結(jié)果顯而易見,方法數(shù)超限了!但是已經(jīng)在build.gradle中配置了multiDexEnabled true和添加了android.support.multidex,為何還會出錯呢? 原來是忘了繼承MultiDexApplication了!敲腦袋ing...
接下來,我們借助官方文檔來了解下64K方法數(shù)限制。
正文
隨著應(yīng)用不斷增加新功能,引入新庫,apk會越來越大,到達一定規(guī)模后就可能遇到方法數(shù)超限問題。
早期版本錯誤信息如下:
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.
其中數(shù)字65536是關(guān)鍵,Android平臺的Java虛擬機Dalvik執(zhí)行Dex程序時,使用的是short類型來索引DEX文件中的方法。這就意味著單個Dex文件可被引用的方法總數(shù)被限制為64x1024, 即65536。其中包括:
- Android Framework的方法
- library的方法
- 我們自己寫的方法
為突破這個限制,需要使用multidex來生成多個dex文件。
Android5.0 (API level 21)之前版本支持Multidex
Android5.0之前使用Dalvik運行時執(zhí)行應(yīng)用代碼,默認Dalvik限制每個apk只能有一個字節(jié)碼classed.dex文件。為突破這個限制,可以使用multidex support library來管理額外的dex文件(包括代碼)。
Android5.0及更高版本支持Multidex
Android5.0及更高版本使用支持從apk中加載多個dex文件的ART運行時機制,在應(yīng)用安裝時,加載classed(...N).dex文件并編譯成一個.oat文件以支持在Android設(shè)備上運行。關(guān)于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之前,你或許可以通過以下方法來減小方法總數(shù)(包括引用的、library里的和自己寫的方法)。
- 排除未使用的依賴 -此步驟通常能有效避免64K限制。
- 使用ProGuard去除未使用的方法 -為release版本配置ProGuard,能有效排除一些無用方法
使用以上技術(shù)能有效避免更改構(gòu)建配置來引用更多的方法,同時能減小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.
如果你的應(yīng)用中已經(jīng)繼承Application,那么可以通過復(fù)寫attachBaseContext()方法并調(diào)用MultiDex.install(this)來支持multidex,即無需修改manifest文件。更多信息請看MultiDexApplication
補充:
亦可直接將繼承Application 改為繼承MultiDexApplication,而無需修改manifest文件或復(fù)寫attachBaseContext()方法。
multidex support library的使用限制
multidex support library有一些已知的限制請務(wù)必知曉,需要在應(yīng)用時先行測試。
如果classes2.dex文件較大,安裝dex文件到設(shè)備的數(shù)據(jù)區(qū)是一個復(fù)雜的過程,可能會導(dǎo)致應(yīng)用程序無響應(yīng)(ANR)的錯誤。在這種情況下,應(yīng)該使用ProGuard盡量減小dex文件的大小且刪除無用的代碼。
在Android 4.0(API Level 14)之前,由于Dalvik linearalloc bug(問題22586),multidex可能是運行失敗。如果希望運行在Level 14之前的Android系統(tǒng)版本,請先確保完整的測試和使用。優(yōu)化代碼可以減少或可能消除這些潛在的問題。
應(yīng)用程序使用了multiedex配置,會造成申請很大的內(nèi)存分配。可能還會引起Dalvik虛擬機的崩潰(問題78035)。此分配限制是在Android 4.0 (API level 14)上增加的,但Android5.0 (API level 21)之前的版本仍有此限制。
multidex構(gòu)建工具不支持指定哪些類必須包含在首個dex文件中,因而可能導(dǎo)致某些library無法使用。
優(yōu)化Multidex的開發(fā)和構(gòu)建
multidex會加長構(gòu)建應(yīng)用的時間,這個必要的過程可能會拖慢你的開發(fā)進度。
為加速構(gòu)建過程,我們可以在Gradle中配置productFlavors: a development flavor and a production flavor.
開發(fā)時將minSdkVersion改為21使用ART運行時機制,這樣能加快構(gòu)建速度。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'
}
完成上述配置后,你可以使用結(jié)合了dev productFlavor和buildType屬性的devDebug變體app。
這個變體app包含如下特性:
- 關(guān)閉了混淆(proguard)
- 支持multidex
- minSdkVersion 設(shè)置為 Android API level 21.
這些設(shè)置將使Gradle插件做如下事情:
- 編譯應(yīng)用的每個模塊(包括依賴)為獨立的dex文件,這個過程稱為
pre-dexing - 不作修改地include每個dex文件到apk里
- 更重要的是,這些模塊dex文件將不會合并,這樣避免分割主dex文件,以加快速度
值得注意的是:上述配置后的devDebug變種app僅能運行在Android 5.0設(shè)備上。
同時,你也可以構(gòu)建其他變體app,也可以在終端使用gradel命令來實現(xiàn)多渠道打包等。更多有關(guān)flavors和Gradle tasks信息, 請看Gradle Plugin User Guide(中文翻譯).
在Android Studio中構(gòu)建變種App
使用multidex時,構(gòu)建變體app對管理構(gòu)建過程是非常有用的。Android studio允許用戶自己選擇。
在Android Studio中構(gòu)建變體app,步驟如下:
- 從左邊欄打開
Build Variants窗口 - 點擊
build variant以選擇不同變體,如圖:
測試Multidex應(yīng)用
測試multidex應(yīng)用,需在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'
}
}
備注:文中鏈接為官方鏈接,請爬墻觀看!