為什么需要對Dex進行分包
Android在安裝應用的過程中,系統(tǒng)會運行一個名為DexOpt的程序為該應用在當前機型中運行做準備。DexOpt 是在第一次加載 Dex 文件的時候執(zhí)行的。這個過程會生成一個 ODEX 文件,即 Optimised Dex。執(zhí)行 ODEX 的效率會比直接執(zhí)行 Dex 文件的效率要高很多。
在開發(fā)應用時,隨著業(yè)務規(guī)模發(fā)展到一定程度,不斷地加入新功能、添加新的類庫,代碼在急劇的膨脹,相應的apk包的大小也急劇增加, 那么終有一天,你會不幸遇到這個錯誤:
- 生成的apk在android 2.3或之前的機器上無法安裝,提示
INSTALL_FAILED_DEXOPT
- 方法數量過多,編譯時出錯,提示:
Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536
原因如下:
- 無法安裝(Android 2.3 INSTALL_FAILED_DEXOPT)問題,是由DexOpt的LinearAlloc限制引起的。DexOpt使用LinearAlloc來存儲應用的方法信息,Dalvik linearAlloc是一個固定大小的緩沖區(qū)。Android 2.2和2.3的緩沖區(qū)只有5MB,Android 4.x提高到了8MB或16MB。當應用的方法數量過多導致超出緩沖區(qū)大小時,會造成dexopt崩潰。
- 超過最大方法數限制的問題,是由于DEX文件格式限制,一個DEX文件中method個數采用使用原生類型short來索引文件中的方法,也就是4個字節(jié)共計最多表達65536個method,field/class的個數也均有此限制。對于DEX文件,則是將工程所需全部class文件合并且壓縮到一個DEX文件期間,也就是Android打包的DEX過程中, 單個DEX文件可被引用的方法總數(自己開發(fā)的代碼以及所引用的Android框架、類庫的代碼)被限制為65536;
MultiDex方案
Google為構建超過65K方法數的應用提供官方支持的方案:MultiDex。
首先使用Android SDK Manager升級到最新的Android SDK Build Tools和Android Support Library。然后進行以下兩步操作:
- 修改Gradle配置文件,啟用MultiDex并包含MultiDex支持:
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'
}
- 讓應用支持多DEX文件。在官方文檔中描述了三種可選方法:
- 在AndroidManifest.xml的application中聲明android.support.MultiDex.MultiDexApplication;
- 如果你已經有自己的Application類,讓其繼承MultiDexApplication;
- 如果你的Application類已經繼承自其它類,你不想/能修改它,那么可以重寫attachBaseContext()方法:
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
MultiDex.install(this);
}
Multidex的局限性
官方文檔中提到了Multidex的局限性:
1.如果第二個(或其他個)dex文件很大的話,安裝.dex文件到data分區(qū)時可能會導致ANR(應用程序無響應),此時應該使用ProGuard減小DEX文件的大小。
2.由于Dalvik linearAlloc的bug的關系,使用了multidex的應用可能無法在Android 4.0 (API level 14)或之前版本的設備上運行。
3.由于Dalvik linearAlloc的限制,使用了multidex的應用會請求非常大的內存分配,從而導致程序奔潰。Dalvik linearAlloc是一個固定大小的緩沖區(qū)。 在應用的安裝過程中,系統(tǒng)會運行一個名為dexopt的程序為該應用在當前機型中運行做準備。dexopt使用LinearAlloc來存儲應用的方法信息。 Android 2.2和2.3的緩沖區(qū)只有5MB,Android 4.x提高到了8MB或16MB。當方法數量過多導致超出緩沖區(qū)大小時,會造成dexopt崩潰。
4.在Dalvik運行時中,某些類的方法必須要放在主dex中,Android構建工具可能無法確保所有有此要求的類被編譯進主dex中。
這些問題也非常值得我們關注。
一些在二級Dex加載之前,可能會被調用到的類(比如靜態(tài)變量的類),需要放在主Dex中,否則會ClassNotFoundError。 通過修改Gradle,可以顯式的把一些類放在Main Dex中。
afterEvaluate {
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
dx.additionalParameters += '--multi-dex'
dx.additionalParameters += "--main-dex-list=$projectDir/<filename>".toString()
}
}
注意上面是修改后的Gradle,其中是一個文本文件的文件名,存放在和這個build.gradle腳本同一級的文件目錄下,而不是項目根目錄??梢园堰@個文本文件起名為multidex.keep,內容如下,實際就是把需要放在Main Dex的類羅列出來。
android/support/multidex/BuildConfig/class
android/support/multidex/MultiDex$V14/class
android/support/multidex/MultiDex$V19/class
android/support/multidex/MultiDex$V4/class
android/support/multidex/MultiDex/class
android/support/multidex/MultiDexApplication/class
android/support/multidex/MultiDexExtractor$1/class
android/support/multidex/MultiDexExtractor/class
android/support/multidex/ZipUtil$CentralDirectory/class
android/support/multidex/ZipUtil/class
project.afterEvaluate標簽在特定的project配置完成后運行,而gradle.projectsEvaluated在所有projects配置完成后運行。 注意afterEvaluate需要放在android{}里,不可放外面。
這樣做了之后并不一定解壓apk之后會出現(xiàn)多個dex文件,可能仍然只有一個dex。因為只有必須分包的時候才會分,如果不需要就不會。 如果要強制分dex,還需要加上dx.additionalParameters += ‘–minimal-main-dex’。完整的配置如下:
afterEvaluate {
tasks.matching {
it.name.startsWith('dex')
}.each { dx ->
if (dx.additionalParameters == null) {
dx.additionalParameters = []
}
dx.additionalParameters += '--multi-dex'
// 設置multidex.keep文件中class為第一個dex文件中包含的class,如果沒有下一項設置此項無作用
dx.additionalParameters += "--main-dex-list=$projectDir/multidex.keep".toString()
//此項添加后第一個classes.dex文件只能包含-main-dex-list列表中class
dx.additionalParameters += '--minimal-main-dex'
}
}
這樣配置了之后就按照multidex.keep里面的內容拆分出了第一個dex文件。其他內容在第二個里面。 那么如何把需要的類放在multidex.keep文件里呢?其實不用手動一個類一個類寫,我們進入這個文件: 項目\build\intermediates\multi-dex\release(或debug)\maindexlist.txt。 將maindexlist.txt中沒有在application中初始化的類刪除一部分之后,剩余的復制到multidex.keep文件中就可以了。 當然也可以自行增加沒有被包含進去的類,因為不直接引用的類都不在maindexlist.txt中。 注意,如果需要混淆的話需要寫混淆之后的 class 。
MultiDex實現(xiàn)原理
1.Dex拆分
dex拆分步驟為:
- 自動掃描整個工程代碼得到main-dex-list;
- 根據main-dex-list對整個工程編譯后的所有class進行拆分,將主、從dex的class文件分開;
- 用dx工具對主、從dex的class文件分別打包成 .dex文件,并放在apk的合適目錄。
怎么自動生成 main-dex-list? Android SDK 從 build tools 21 開始提供了 mainDexClasses 腳本來生成主 dex 的文件列表。查看這個腳本的源碼,可以看到它主要做了下面兩件事情:
1)調用 proguard 的 shrink 操作來生成一個臨時 jar 包;
2)將生成的臨時 jar 包和輸入的文件集合作為參數,然后調用com.android.multidex.MainDexListBuilder 來生成主 dex 文件列表。
2.Dex加載
因為Android系統(tǒng)在啟動應用時只加載了主dex(Classes.dex),其他的 dex 需要我們在應用啟動后進行動態(tài)加載安裝。android-support-multidex.jar就是做這個用的,該 jar 包從 build tools 21.1 開始支持。
android系統(tǒng)使用BaseDexClassLoader來加載Dex文件,它有兩個子類DexClassLoader和PathClassLoader,它們使用場景如下:
- PathClassLoader是Android應用中的默認加載器,PathClassLoader只能加載/data/app中的apk,也就是已經安裝到手機中的apk。這個也是PathClassLoader作為默認的類加載器的原因,因為一般程序都是安裝了,在打開,這時候PathClassLoader就去加載指定的apk(解壓成dex,然后在優(yōu)化成odex)就可以了。
- DexClassLoader可以加載任何路徑的apk/dex/jar,PathClassLoader只能加載已安裝到系統(tǒng)中(即/data/app目錄下)的apk文件。
基本實現(xiàn)原理:
1、除了第一個dex文件(即正常apk包唯一包含的Dex文件),其它dex文件都以資源的方式放在安裝包中。所以我們需要將其他dex文件并在Application的onCreate回調中注入到系統(tǒng)的ClassLoader。并且對于那些在注入之前已經引用到的類(以及它們所在的jar),必須放入第一個Dex文件中。
2、PathClassLoader作為默認的類加載器,在打開應用程序的時候PathClassLoader就去加載指定的apk(解壓成dex,然后在優(yōu)化成odex),也就是第一個dex文件是PathClassLoader自動加載的。所以,我們需要做的就是將其他的dex文件注入到這個PathClassLoader中去。
3、因為PathClassLoader和DexClassLoader的原理基本一致,從前面的分析來看,我們知道PathClassLoader里面的dex文件是放在一個Element數組里面,可以包含多個dex文件,每個dex文件是一個Element,所以我們只需要將其他的dex文件放到這個數組中去就可以了。
實現(xiàn):
1、通過反射獲取PathClassLoader中的DexPathList中的Element數組(已加載了第一個dex包,由系統(tǒng)加載)
2、通過反射獲取DexClassLoader中的DexPathList中的Element數組(將第二個dex包加載進去)
3、將兩個Element數組合并之后,再將其賦值給PathClassLoader的Element數組