Android Dex分包方案和熱補(bǔ)丁原理

為什么需要對Dex進(jìn)行分包

Android在安裝應(yīng)用的過程中,系統(tǒng)會運(yùn)行一個(gè)名為DexOpt的程序?yàn)樵搼?yīng)用在當(dāng)前機(jī)型中運(yùn)行做準(zhǔn)備。DexOpt 是在第一次加載 Dex 文件的時(shí)候執(zhí)行的。這個(gè)過程會生成一個(gè) ODEX 文件,即 Optimised Dex。執(zhí)行 ODEX 的效率會比直接執(zhí)行 Dex 文件的效率要高很多。
在開發(fā)應(yīng)用時(shí),隨著業(yè)務(wù)規(guī)模發(fā)展到一定程度,不斷地加入新功能、添加新的類庫,代碼在急劇的膨脹,相應(yīng)的apk包的大小也急劇增加, 那么終有一天,你會不幸遇到這個(gè)錯(cuò)誤:

  1. 生成的apk在android 2.3或之前的機(jī)器上無法安裝,提示

INSTALL_FAILED_DEXOPT

  1. 方法數(shù)量過多,編譯時(shí)出錯(cuò),提示:

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

原因如下:

  1. 無法安裝(Android 2.3 INSTALL_FAILED_DEXOPT)問題,是由DexOpt的LinearAlloc限制引起的。DexOpt使用LinearAlloc來存儲應(yīng)用的方法信息,Dalvik linearAlloc是一個(gè)固定大小的緩沖區(qū)。Android 2.2和2.3的緩沖區(qū)只有5MB,Android 4.x提高到了8MB或16MB。當(dāng)應(yīng)用的方法數(shù)量過多導(dǎo)致超出緩沖區(qū)大小時(shí),會造成dexopt崩潰。
  2. 超過最大方法數(shù)限制的問題,是由于DEX文件格式限制,一個(gè)DEX文件中method個(gè)數(shù)采用使用原生類型short來索引文件中的方法,也就是4個(gè)字節(jié)共計(jì)最多表達(dá)65536個(gè)method,field/class的個(gè)數(shù)也均有此限制。對于DEX文件,則是將工程所需全部class文件合并且壓縮到一個(gè)DEX文件期間,也就是Android打包的DEX過程中, 單個(gè)DEX文件可被引用的方法總數(shù)(自己開發(fā)的代碼以及所引用的Android框架、類庫的代碼)被限制為65536;

MultiDex方案

Google為構(gòu)建超過65K方法數(shù)的應(yīng)用提供官方支持的方案:MultiDex。
首先使用Android SDK Manager升級到最新的Android SDK Build Tools和Android Support Library。然后進(jìn)行以下兩步操作:

  1. 修改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'
}
  1. 讓應(yīng)用支持多DEX文件。在官方文檔中描述了三種可選方法:
  • 在AndroidManifest.xml的application中聲明android.support.MultiDex.MultiDexApplication;
  • 如果你已經(jīng)有自己的Application類,讓其繼承MultiDexApplication;
  • 如果你的Application類已經(jīng)繼承自其它類,你不想/能修改它,那么可以重寫attachBaseContext()方法:
@Override 
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    MultiDex.install(this);
}

Multidex的局限性

官方文檔中提到了Multidex的局限性:

1.如果第二個(gè)(或其他個(gè))dex文件很大的話,安裝.dex文件到data分區(qū)時(shí)可能會導(dǎo)致ANR(應(yīng)用程序無響應(yīng)),此時(shí)應(yīng)該使用ProGuard減小DEX文件的大小。
2.由于Dalvik linearAlloc的bug的關(guān)系,使用了multidex的應(yīng)用可能無法在Android 4.0 (API level 14)或之前版本的設(shè)備上運(yùn)行。
3.由于Dalvik linearAlloc的限制,使用了multidex的應(yīng)用會請求非常大的內(nèi)存分配,從而導(dǎo)致程序奔潰。Dalvik linearAlloc是一個(gè)固定大小的緩沖區(qū)。 在應(yīng)用的安裝過程中,系統(tǒng)會運(yùn)行一個(gè)名為dexopt的程序?yàn)樵搼?yīng)用在當(dāng)前機(jī)型中運(yùn)行做準(zhǔn)備。dexopt使用LinearAlloc來存儲應(yīng)用的方法信息。 Android 2.2和2.3的緩沖區(qū)只有5MB,Android 4.x提高到了8MB或16MB。當(dāng)方法數(shù)量過多導(dǎo)致超出緩沖區(qū)大小時(shí),會造成dexopt崩潰。
4.在Dalvik運(yùn)行時(shí)中,某些類的方法必須要放在主dex中,Android構(gòu)建工具可能無法確保所有有此要求的類被編譯進(jìn)主dex中。

這些問題也非常值得我們關(guān)注。

一些在二級Dex加載之前,可能會被調(diào)用到的類(比如靜態(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,其中是一個(gè)文本文件的文件名,存放在和這個(gè)build.gradle腳本同一級的文件目錄下,而不是項(xiàng)目根目錄??梢园堰@個(gè)文本文件起名為multidex.keep,內(nèi)容如下,實(shí)際就是把需要放在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標(biāo)簽在特定的project配置完成后運(yùn)行,而gradle.projectsEvaluated在所有projects配置完成后運(yùn)行。 注意afterEvaluate需要放在android{}里,不可放外面。

這樣做了之后并不一定解壓apk之后會出現(xiàn)多個(gè)dex文件,可能仍然只有一個(gè)dex。因?yàn)橹挥斜仨毞职臅r(shí)候才會分,如果不需要就不會。 如果要強(qiáng)制分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'
            // 設(shè)置multidex.keep文件中class為第一個(gè)dex文件中包含的class,如果沒有下一項(xiàng)設(shè)置此項(xiàng)無作用
            dx.additionalParameters += "--main-dex-list=$projectDir/multidex.keep".toString()
            //此項(xiàng)添加后第一個(gè)classes.dex文件只能包含-main-dex-list列表中class  
            dx.additionalParameters += '--minimal-main-dex'
        }
    }

這樣配置了之后就按照multidex.keep里面的內(nèi)容拆分出了第一個(gè)dex文件。其他內(nèi)容在第二個(gè)里面。 那么如何把需要的類放在multidex.keep文件里呢?其實(shí)不用手動一個(gè)類一個(gè)類寫,我們進(jìn)入這個(gè)文件: 項(xiàng)目\build\intermediates\multi-dex\release(或debug)\maindexlist.txt。 將maindexlist.txt中沒有在application中初始化的類刪除一部分之后,剩余的復(fù)制到multidex.keep文件中就可以了。 當(dāng)然也可以自行增加沒有被包含進(jìn)去的類,因?yàn)椴恢苯右玫念惗疾辉趍aindexlist.txt中。 注意,如果需要混淆的話需要寫混淆之后的 class 。

MultiDex實(shí)現(xiàn)原理

1.Dex拆分

dex拆分步驟為:

  1. 自動掃描整個(gè)工程代碼得到main-dex-list;
  2. 根據(jù)main-dex-list對整個(gè)工程編譯后的所有class進(jìn)行拆分,將主、從dex的class文件分開;
  3. 用dx工具對主、從dex的class文件分別打包成 .dex文件,并放在apk的合適目錄。

怎么自動生成 main-dex-list? Android SDK 從 build tools 21 開始提供了 mainDexClasses 腳本來生成主 dex 的文件列表。查看這個(gè)腳本的源碼,可以看到它主要做了下面兩件事情:

1)調(diào)用 proguard 的 shrink 操作來生成一個(gè)臨時(shí) jar 包;
2)將生成的臨時(shí) jar 包和輸入的文件集合作為參數(shù),然后調(diào)用com.android.multidex.MainDexListBuilder 來生成主 dex 文件列表。

2.Dex加載

因?yàn)锳ndroid系統(tǒng)在啟動應(yīng)用時(shí)只加載了主dex(Classes.dex),其他的 dex 需要我們在應(yīng)用啟動后進(jìn)行動態(tài)加載安裝。android-support-multidex.jar就是做這個(gè)用的,該 jar 包從 build tools 21.1 開始支持。
android系統(tǒng)使用BaseDexClassLoader來加載Dex文件,它有兩個(gè)子類DexClassLoader和PathClassLoader,它們使用場景如下:

  • PathClassLoader是Android應(yīng)用中的默認(rèn)加載器,PathClassLoader只能加載/data/app中的apk,也就是已經(jīng)安裝到手機(jī)中的apk。這個(gè)也是PathClassLoader作為默認(rèn)的類加載器的原因,因?yàn)橐话愠绦蚨际前惭b了,在打開,這時(shí)候PathClassLoader就去加載指定的apk(解壓成dex,然后在優(yōu)化成odex)就可以了。
  • DexClassLoader可以加載任何路徑的apk/dex/jar,PathClassLoader只能加載已安裝到系統(tǒng)中(即/data/app目錄下)的apk文件。

基本實(shí)現(xiàn)原理:
1、除了第一個(gè)dex文件(即正常apk包唯一包含的Dex文件),其它dex文件都以資源的方式放在安裝包中。所以我們需要將其他dex文件并在Application的onCreate回調(diào)中注入到系統(tǒng)的ClassLoader。并且對于那些在注入之前已經(jīng)引用到的類(以及它們所在的jar),必須放入第一個(gè)Dex文件中。

2、PathClassLoader作為默認(rèn)的類加載器,在打開應(yīng)用程序的時(shí)候PathClassLoader就去加載指定的apk(解壓成dex,然后在優(yōu)化成odex),也就是第一個(gè)dex文件是PathClassLoader自動加載的。所以,我們需要做的就是將其他的dex文件注入到這個(gè)PathClassLoader中去。

3、因?yàn)镻athClassLoader和DexClassLoader的原理基本一致,從前面的分析來看,我們知道PathClassLoader里面的dex文件是放在一個(gè)Element數(shù)組里面,可以包含多個(gè)dex文件,每個(gè)dex文件是一個(gè)Element,所以我們只需要將其他的dex文件放到這個(gè)數(shù)組中去就可以了。

實(shí)現(xiàn):
1、通過反射獲取PathClassLoader中的DexPathList中的Element數(shù)組(已加載了第一個(gè)dex包,由系統(tǒng)加載)
2、通過反射獲取DexClassLoader中的DexPathList中的Element數(shù)組(將第二個(gè)dex包加載進(jìn)去)
3、將兩個(gè)Element數(shù)組合并之后,再將其賦值給PathClassLoader的Element數(shù)組

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容