dex分包變形記
一、背景
就在項(xiàng)目灰度測(cè)試前不久,爆出了在 Android 3.0以下手機(jī)上安裝時(shí)出現(xiàn) INSTALL _ FAILED_DEXOPT,導(dǎo)致安裝失敗。這一問(wèn)題意味著項(xiàng)目將不能在 Android 3.0以下的手機(jī)上安裝使用,對(duì)項(xiàng)目的發(fā)布有比較大的影響,所以必須盡快解決。
INSTALL _ FAILED_DEXOPT導(dǎo)致無(wú)法安裝的問(wèn)題,從根本上來(lái)說(shuō),可能是兩個(gè)原因造成的:
(1) 單個(gè) dex 文件方法總數(shù)65K 的限制。
(2) Dexopt 的 LinearAlloc 限制。
當(dāng) Android 系統(tǒng)安裝一個(gè)應(yīng)用的時(shí)候,有一步是對(duì) Dex 進(jìn)行優(yōu)化,這個(gè)過(guò)程有一個(gè)專(zhuān)門(mén)的工具來(lái)處理,叫 DexOpt。DexOpt 是在***次加載 Dex 文件的時(shí)候執(zhí)行的。這個(gè)過(guò)程會(huì)生成一個(gè) ODEX 文件,即 Optimised Dex。執(zhí)行 ODEX 的效率會(huì)比直接執(zhí)行 Dex 文件的效率要高很多。
但是在早期的 Android 系統(tǒng)中,DexOpt 有兩個(gè)問(wèn)題。(一):DexOpt 會(huì)把每一個(gè)類(lèi)的方法 id 檢索起來(lái),存在一個(gè)鏈表結(jié)構(gòu)里面,但是這個(gè)鏈表的長(zhǎng)度是用一個(gè) short 類(lèi)型來(lái)保存的,導(dǎo)致了方法 id 的數(shù)目不能夠超過(guò)65536個(gè)。當(dāng)一個(gè)項(xiàng)目足夠大的時(shí)候,顯然這個(gè)方法數(shù)的上限是不夠的。(二):Dexopt 使用 LinearAlloc 來(lái)存儲(chǔ)應(yīng)用的方法信息。Dalvik LinearAlloc 是一個(gè)固定大小的緩沖區(qū)。在Android 版本的歷史上,LinearAlloc 分別經(jīng)歷了4M/5M/8M/16M限制。Android 2.2和2.3的緩沖區(qū)只有5MB,Android 4.x提高到了8MB 或16MB。當(dāng)方法數(shù)量過(guò)多導(dǎo)致超出緩沖區(qū)大小時(shí),也會(huì)造成dexopt崩潰。
盡管在新版本的 Android 系統(tǒng)中,DexOpt 修復(fù)了方法數(shù)65K的限制問(wèn)題,并且擴(kuò)大了 LinearAlloc 限制,但是我們?nèi)匀恍枰獙?duì)低版本的 Android 系統(tǒng)做兼容。
回頭說(shuō)項(xiàng)目。由于項(xiàng)目新版本新增功能點(diǎn)和代碼較多,在方法數(shù)減無(wú)可減的時(shí)候,仍然不能解決INSTALL _ FAILED _ DEXOPT的問(wèn)題。所以,最終我們采用了 dex 分包的方案,來(lái)避開(kāi)了 Android 3.0以下平臺(tái)的方法數(shù)和 LinearAlloc 限制。
簡(jiǎn)單的說(shuō),分包就是在打包時(shí)將應(yīng)用的代碼分成多個(gè) dex,使得主 dex 的方法數(shù)和所需的 LinearAlloc 不超過(guò)系統(tǒng)限制。在應(yīng)用啟動(dòng)或運(yùn)行過(guò)程中,首先是主 dex 啟動(dòng)運(yùn)行后,再加載從 dex,這樣就繞開(kāi)了這兩個(gè)限制。
這樣,我們的分包方案就要解決兩個(gè)問(wèn)題:一是如何對(duì) dex 進(jìn)行拆分,二是如何加載從 dex。
二、Google 官方方案
1.Dex 拆分
首先,我們需要解決如何對(duì)dex進(jìn)行拆分?
通過(guò)學(xué)習(xí)資料,我們知道,對(duì)于方法數(shù)超過(guò)65K 的問(wèn)題,Google 官方從 Android Build tools 21.1就開(kāi)始著手解決了。
先看官方網(wǎng)站提供的配置。Google MultiDex 官方文檔是針對(duì) Gradle 進(jìn)行配置的,如下:
- 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'
- }
那么,是不是按 Google 官方文檔配置一下就 OK 了呢?不管怎樣,這是官方提供的方案,而且是最直接的做法,所以我們應(yīng)該先試一試。
因?yàn)槲覀冺?xiàng)目的 RDM 構(gòu)建環(huán)境采用的是 ant 腳本編譯,所以首先要想辦法把 Google 官方編譯配置改造成 ant 腳本。
官 方文檔上只提供了如何使用 MultiDex,沒(méi)有說(shuō)明構(gòu)建時(shí)如何打包出多個(gè) dex。其實(shí)是因?yàn)槿绻昧诉@種 Gradle來(lái)構(gòu)建,當(dāng)應(yīng)用構(gòu)建時(shí),構(gòu)建工具會(huì)自動(dòng)分析哪些類(lèi)必須放在***個(gè) DEX 文件(主 dex),哪些類(lèi)可以放在附加的 DEX 文件(從 dex)中,并將分析結(jié)果輸出到 dx 進(jìn)行后續(xù)打包。當(dāng)它創(chuàng)建了主 dex 文件(classes.dex)后,如果有必要會(huì)繼續(xù)創(chuàng)建從 DEX 文件,如 classes2.dex, classes3.dex。這種方法優(yōu)點(diǎn)是配置比較簡(jiǎn)單,但是***的缺點(diǎn)是不能指定哪些類(lèi)必須包含在主 dex 中,容易導(dǎo)致應(yīng)用啟動(dòng)時(shí)某些類(lèi)找不到,出現(xiàn) Class Not Found Exception。
我們把上述 Gradle 的配置改成 ant 腳本時(shí),就不能簡(jiǎn)單套用了。通過(guò)查看 dx 工具的用法:
參數(shù)說(shuō)明:
--multi-dex:多 dex 打包的開(kāi)關(guān)。
--main-dex-list=:參數(shù)是一個(gè)類(lèi)列表的文件,在該文件中的類(lèi)會(huì)被打包在***個(gè) dex 中。
--minimal-main-dex:只有在--main-dex-list 文件中指定的類(lèi)被打包在***個(gè) dex,其余的都在第二個(gè) dex 文件中。
因?yàn)楹髢蓚€(gè)參數(shù)是 optional 參數(shù),所以理論上只需給 dx 加上“--multi-dex”參數(shù)即可生成出 classes.dex、classes2.dex、classes3.dex、…。
在 Gradle 中可以做如下的配置:
- afterEvaluate {
- tasks.matching {
- it.name.startsWith('dex')
- }.each { dx ->
- if (dx.additionalParameters == null) {
- dx.additionalParameters = ['--multi-dex']
- } else {
- dx.additionalParameters += '--multi-dex'
- }
- }
- }
好了,這樣我們就可以改造我們的 ant 腳本了。
改造的方法是在項(xiàng)目打包的 ant 腳本中引入 Android build Tools 21.1.2,并把用 dx 生成 dex 的部分改造成下面的樣子:
編譯、打包,并沒(méi)有像預(yù)期那樣生成多個(gè) dex,而是只生成了一個(gè) classes.dex:
生成的 apk 包跟 dex 分包前一樣。為什么會(huì)這樣?
再看 dx 的參數(shù),main-dex-list 和 minimal-main-dex 只會(huì)影響到主 dex 中包含的文件,不會(huì)影響到從 dex 是否生成,所以應(yīng)該是其他原因造成的。
查不到資料,分析源代碼就是解決問(wèn)題的不二法門(mén)。于是我把 dx.jar 反編譯了一下,通過(guò)分析,找到了下面的幾行關(guān)鍵代碼:
顯然,dx 進(jìn)行多 dex 打包時(shí),默認(rèn)每個(gè) dex 中方法數(shù)***為65536。而查看當(dāng)前應(yīng)用 dex 的方法數(shù),一共只有51392(方法數(shù)沒(méi)超標(biāo),主要是 LinearAlloc 超標(biāo)),沒(méi)有達(dá)到65536,所以打包的時(shí)候只有一個(gè) dex。
再繼續(xù)分析代碼,發(fā)現(xiàn)下面一段關(guān)鍵代碼:
這說(shuō)明 dx 有一個(gè)隱藏的參數(shù):--set-max-idx-number,這個(gè)參數(shù)可以用于設(shè)置 dx 打包時(shí)每個(gè) dex ***的方法數(shù),但是此參數(shù)并未在 dx 的 Usage 中列出(坑爹?。。?。
我們?cè)?ant 腳本中把這個(gè)參數(shù)設(shè)置上,暫時(shí)設(shè)置每個(gè) dex 的方法數(shù)***為48000:
重新打包,結(jié)果如下:
果然,第二個(gè) dex 出現(xiàn)了!
可是,觀察一下 res 目錄,這里出現(xiàn)了一個(gè)新的問(wèn)題,drawable 密度后綴的資源目錄都多了一個(gè) v4:
為 什么這幾個(gè)目錄會(huì)帶 v4后綴呢?原來(lái)這是 R6以上的 Android SDK Tools 自動(dòng)打包工具新加的一個(gè)處理,即為這些在 Android 1.0 時(shí)不存在的密度后綴命名的資源路徑名稱(chēng)后面自動(dòng)添加一個(gè)適合的版本后綴,以確保老版本不使用這些資源(只有 API level 4以及更高版本支持后綴),v4 就表示使用在 Android 1.6 或更高版本。
上述的 Dex 拆分過(guò)程采用的就是 Google 官方的方案。Dex 拆分已經(jīng)完成,如何加載呢?
2.Dex加載
因?yàn)?Android 系統(tǒng)在啟動(dòng)應(yīng)用時(shí)只加載了主 dex(Classes.dex),其他的 dex 需要我們?cè)趹?yīng)用啟動(dòng)后進(jìn)行動(dòng)態(tài)加載安裝。
Google 官方方案是如何加載的呢?
Google 官方支持 Multidex 的 jar 包是 android-support-multidex.jar,該 jar 包從 build tools 21.1 開(kāi)始支持。這個(gè) jar 加載 apk 中的從 dex 流程如下:
此處主要的工作就是從 apk 中提取出所有的從 dex(classes2.dex,classes3.dex,…),然后通過(guò)反射依次安裝加載從 dex 并合并 DexPathList 的 Element 數(shù)組。
如果引用這個(gè) jar 包,MultiDexApplication 的 Java Doc 提供了三種方式來(lái)加載從 dex:
1)在 AndroidManifest.xml 中,把 application 定義為 android.support.multidex.MultiDexApplication。
2)用自定義的 Application 類(lèi)繼承 android.support.multidex.MultiDexApplication,再配置 application 為自定義的類(lèi)。
3) 如果之前自定義的 Application 類(lèi)已經(jīng)繼承了其他 Application 類(lèi),而且不想改變,那么可以重寫(xiě)自定義 Application 類(lèi)的 attachBaseContext() 或者 onCreate() 方法,并添加語(yǔ)句 MultiDex.install(this)。
為了使改動(dòng)最小,我們采用上述3)中的調(diào)用方式:
到此為止,用 Google 官方方案進(jìn)行 dex 拆分和加載就已經(jīng)完成了。安裝運(yùn)行一下試試!
3.安裝運(yùn)行
我們把分包后的 apk 在 Android 4.3的手機(jī)上進(jìn)行安裝。沒(méi)有問(wèn)題,順利安裝上了!
沒(méi)想到的是,啟動(dòng)時(shí)沒(méi)出現(xiàn)任何頁(yè)面,直接 crash。Crash 的 log 如下:
從 log 上看,項(xiàng)目在啟動(dòng)閃屏頁(yè)面時(shí)無(wú)法實(shí)例化 com.example.AppService.AstApp,因?yàn)檎也坏? com.example.AppService.AstApp 這個(gè)類(lèi)。既然 Application 類(lèi)都找不到,那么我們?cè)?Application 中加載從 dex 更加沒(méi)有執(zhí)行到了。
反編譯一下 classes.dex 和 classes2.dex,果然 com.example.AppService.AstApp 是在classes2.dex,所以剛啟動(dòng)時(shí)在主 dex(classes.dex) 中找不到 com.example.AppService.AstApp(Application 類(lèi))。
理 論上,啟動(dòng)必需的代碼應(yīng)該放在主 dex 中,這些代碼包括 Application、BaseActivity 等代碼以及繼承自它們的代碼的一個(gè)依賴(lài)集。但是我們看到,單純依賴(lài)于構(gòu)建工具自動(dòng)進(jìn)行 dex 拆分時(shí),我們無(wú)法決定或干預(yù)哪些類(lèi)應(yīng)該放在主 dex,哪些類(lèi)應(yīng)該放在從 dex,這就可能導(dǎo)致啟動(dòng)時(shí)往往會(huì)有類(lèi)庫(kù)找不到。
接下來(lái),我們就得想辦法來(lái)自主定制主、從 dex 包含的文件,使它們完全可控。
4.Google 官方方案的小結(jié)
采用 Google 官方的拆包方案走到現(xiàn)在,我們需要再梳理一下思路了。
到現(xiàn)在為止,已經(jīng)解決的問(wèn)題是:
1)能正常打出多個(gè) dex;
2)可以指定每個(gè) dex 的大小;
3)可以加載多個(gè) dex。
尚未解決的問(wèn)題是:如何指定哪些類(lèi)應(yīng)該放到主 dex,哪些類(lèi)應(yīng)該放到從 dex?
關(guān) 于這個(gè)問(wèn)題,從前面 dx 工具的用法中可得知,我們可以在 dx 的參數(shù)中加入--main-dex-list,指定哪些類(lèi)應(yīng)該放在主 dex 中(也可同時(shí)配合使用參數(shù)--minimal-main-dex,指定主 dex 中只包含在--main-dex-list 文件中指定的類(lèi))。
可是問(wèn)題又來(lái)了,怎么得到 main-dex-list 文件?在大的工程開(kāi)發(fā)中,手動(dòng)添加文件列表顯然不現(xiàn)實(shí)。
同時(shí),在前面研究和驗(yàn)證 Google 官方方案的過(guò)程中,也有幾個(gè)不得不提的問(wèn)題:
1)需要高版本的 build Tools、SDK Tools 編譯打包;
2)編譯打包 apk 后生成的 drawable 密度后綴目錄被添加了 v4 后綴;
3)Google 的 MultiDex 方案在運(yùn)行中需要比較大的 LinearAlloc,但是由于 Android 4.0 (API level 14) 以下的機(jī)器上 Dalvik LinearAlloc 的一個(gè)缺陷 (Issue 22586) 和限制 (Issue 78035),可能導(dǎo)致運(yùn)行時(shí)無(wú)法滿(mǎn)足 LinearAlloc 的需求而造成 DexOpt 失敗或者 Dalvik 虛擬機(jī)崩潰;
4)從 dex 不能太大,否則在運(yùn)行時(shí)安裝加載從 dex 的過(guò)程比較復(fù)雜和耗時(shí),可能會(huì)導(dǎo)致應(yīng)用程序無(wú)響應(yīng) (ANR) 的錯(cuò)誤。
由 于項(xiàng)目是***做分包,安裝包改動(dòng)已經(jīng)比較大了,如果再將一直使用且沒(méi)有問(wèn)題的 build Tools、SDK Tools 冒然升級(jí)以及 drawable 密度后綴目錄改變,那么無(wú)論怎樣,它們所帶來(lái)的風(fēng)險(xiǎn)和挑戰(zhàn)都是比較大的,也會(huì)帶來(lái)后期測(cè)試和維護(hù)的工作量。所以,我們的方案一定要做到盡量減少這些改變。 而對(duì)于后面兩點(diǎn),我們就應(yīng)該考慮對(duì) dex 的拆分進(jìn)行干預(yù),使每個(gè) dex 的大小在一定的合理范圍內(nèi),消除或減少觸發(fā) Dalvik LinearAlloc 缺陷和限制的概率以及分包引起的 ANR。
綜合以上幾點(diǎn),我們就需要在對(duì)官方方案透徹研究的基礎(chǔ)上,自己實(shí)現(xiàn)工具腳本來(lái)進(jìn)行 dex 的自主拆分、加載,便于靈活的適應(yīng)低版本 Android SDK tools 以及 Android 平臺(tái)。
三、DEX 自動(dòng)拆包和動(dòng)態(tài)加載方案
1.Dex 拆分
根據(jù)前面對(duì)官方方案的研究總結(jié),我們可以很快梳理出下面幾個(gè)dex拆分步驟:
1)自動(dòng)掃描整個(gè)工程代碼得到 main-dex-list;
2)根據(jù) main-dex-list 對(duì)整個(gè)工程編譯后的所有 class 進(jìn)行拆分,將主、從 dex 的 class 文件分開(kāi);
3)用 dx 工具對(duì)主、從 dex 的 class 文件分別打包成 .dex 文件,并放在 apk 的合適目錄。
怎么自動(dòng)生成 main-dex-list?
Android SDK 從 build tools 21 開(kāi)始提供了 mainDexClasses 腳本來(lái)生成主 dex 的文件列表。查看這個(gè)腳本的源碼,可以看到它主要做了下面兩件事情:
1)調(diào)用 proguard 的 shrink 操作來(lái)生成一個(gè)臨時(shí) jar 包;
2)將生成的臨時(shí) jar 包和輸入的文件集合作為參數(shù),然后調(diào)用com.android.multidex.MainDexListBuilder 來(lái)生成主 dex 文件列表。
Proguard的官網(wǎng)執(zhí)行步驟如下:
在 shrink 這一步,proguard 會(huì)根據(jù) keep 規(guī)則保留需要的類(lèi)和類(lèi)成員,并丟棄不需要的類(lèi)和類(lèi)成員。也就是說(shuō),上面 shrink 步驟生成的臨時(shí) jar 包里面保留了符合 keep 規(guī)則的類(lèi),這些類(lèi)是需要放在主 dex 中的入口類(lèi)。
但 是僅有這些入口類(lèi)放在主 dex 還不夠,還要找出入口類(lèi)引用的其他類(lèi),不然仍然會(huì)在啟動(dòng)時(shí)出現(xiàn) NoClassDefFoundError。而找出這些引用類(lèi),就是調(diào)用的 com.android.multidex.MainDexListBuilder,它的部分核心代碼如下:
在調(diào)用 com.android.multidex.MainDexListBuilder 之后,符合 keep 規(guī)則的主 dex 文件列表就生成了。
既 然 Android SDK 已經(jīng)提供了這樣一種比較方便的工具,我們就不再重復(fù)發(fā)明輪子了。所以我們首先把 mainDexClasses 腳本進(jìn)行了一些適當(dāng)?shù)母脑?,然后移植?RDM 構(gòu)建環(huán)境下,然后根據(jù)項(xiàng)目代碼的實(shí)際情況將主要的基礎(chǔ)類(lèi)、common 類(lèi)、wakeup 類(lèi)做為補(bǔ)充規(guī)則加入掃描規(guī)則中,再加上基本規(guī)則 Application、Activity、Service、Provider、Receiver 等類(lèi),就組成了項(xiàng)目的主 dex 掃描規(guī)則。
這時(shí),新的問(wèn)題是,由于項(xiàng)目編譯打包時(shí)有代碼混淆的步驟,那我們掃描主 dex 文件列表時(shí)到底是在代碼混淆之前還是之后?理論上,混淆前后都可以?huà)呙?,但是混淆之后掃描時(shí)主要的問(wèn)題是:在制定 keep 規(guī)則時(shí),最合理的方式是采用包路徑來(lái)制定規(guī)則,而混淆后的代碼中大部分包路徑被混淆了,我們無(wú)法根據(jù)混淆后的包路徑來(lái)制定 keep 規(guī)則,也就無(wú)法完全指定哪些文件應(yīng)該放在主 dex 中。所以,結(jié)論就是,我們必須在代碼混淆之前掃描生成主 dex 文件列表。
再往下做 時(shí),問(wèn)題又出現(xiàn)了,我們是在掃描生成主 dex 文件列表后就立刻將主、從 dex 的 class 文件拆分到不同目錄,然后各自進(jìn)行代碼混淆呢還是統(tǒng)一混淆后再進(jìn)行 class 文件的拆分呢?答案是,我們需要統(tǒng)一混淆后再做拆分。因?yàn)槿绻鸱趾蟾髯曰煜?,則必然會(huì)造成混淆后主、從 dex 引用類(lèi)名的不一致,從而導(dǎo)致應(yīng)用無(wú)法正常運(yùn)行。
但是,這樣又有了新的問(wèn)題,我們是在代碼混淆之前掃描生成的主 dex 文件列表,當(dāng)代碼混淆之后,大部分類(lèi)名稱(chēng)和路徑都改變了,我們又如何根據(jù)主 dex 文件列表做拆分呢?答案是,因?yàn)?proguard 做代碼混淆時(shí)生成了一個(gè)混淆前后代碼之間的 mapping 關(guān)系文件,我們只需要根據(jù)這個(gè) mapping 文件進(jìn)行映射,即可得到混淆后的主 dex 文件列表。
到此為止,思路已經(jīng)梳理得比較清楚了。
按照這個(gè)思路,很快就實(shí)現(xiàn)了工具腳本,完成了對(duì)主、從 dex 的拆分。這樣就實(shí)現(xiàn)了主、從 dex 的靈活的生成和定制,不僅解決了前面 Google 官方方案存在的問(wèn)題,而且也為將來(lái)從 dex 的異步加載、按需加載提供了比較好的基礎(chǔ)。
***,項(xiàng)目的從 dex 是打成 jar 包放在 assets 目錄,如下圖所示:
2.Dex加載
Google 官方提供的 android-support-multidex.jar 可以用來(lái)加載官方方案打包的 dex,也完全可以用于加載我們自己的方案打包的 dex,但是這種方式有下面幾個(gè)不利的地方:
1)靈活性不夠,需要所有的從 dex 跟主 dex 在同一級(jí)目錄,即都在 apk 的根目錄,而且從 dex 的命名要符合 classes2.dex、classes3.dex、…、classes(N).dex。
2)該 jar 包提供的是同步加載方式,而且是啟動(dòng)時(shí)一次性加載所有的從 dex,但是從項(xiàng)目分包的需求以及其他產(chǎn)品的經(jīng)驗(yàn)來(lái)看,加載接口提供異步加載和按需加載的能力是很有必要的。
因 此,我們的加載方案需要有比較好的靈活性以及提供同步加載、異步加載、按需加載的能力。根據(jù)這些要求,我們研究了網(wǎng)上一些開(kāi)源的代碼(也包括 Google 官方 android-support-multidex.jar 的代碼),然后經(jīng)過(guò)改造和驗(yàn)證,實(shí)現(xiàn)了一種比較靈活的加載方案。
跟 Google 官方加載方案一樣,這個(gè)方案采用的也是運(yùn)行時(shí)動(dòng)態(tài)加載的方式,利用了 Dalvik 虛擬機(jī)的類(lèi)加載器。
我 們知道,在 Java 虛擬機(jī)里動(dòng)態(tài)加載用的是 ClassLoader。但是在 Dalvik 虛擬機(jī)里,卻不是 ClassLoader,Android 為我們從 ClassLoader 派生出了兩個(gè)類(lèi):DexClassLoader 和 PathClassLoader。這兩者的區(qū)別就是 PathClassLoader 不能主動(dòng)從 zip 包中釋放出 dex,因此只支持直接操作 dex 格式文件,或者已經(jīng)安裝的 apk(因?yàn)橐呀?jīng)安裝的 apk 在 cache 中存在緩存的 dex 文件);而 DexClassLoader 可以支持 .apk、.jar 和 .dex文件,并且會(huì)在指定的 outpath 路徑釋放出 dex 文件。
由于前面說(shuō)了,在安裝包里有多個(gè) dex 時(shí),應(yīng)用安裝時(shí)不會(huì)主動(dòng)釋放從 dex,所以我們需要用 DexClassLoader 來(lái)釋放加載從 dex。當(dāng)需要加載從 dex 時(shí),加載邏輯會(huì)先從 apk 相應(yīng)的目錄釋放出所需加載的從 dex,然后執(zhí)行加載。
加載過(guò)程的部分核心代碼如下:
上 述代碼是通過(guò)反射獲取 PathClassLoader 中的 DexPathList 中的 Element 數(shù)組(加載主 dex 后的 Element 數(shù)組)和 DexClassLoader 中的 DexPathList 中的 Element 數(shù)組(加載從 dex 后的 Element 數(shù)組),然后將兩個(gè) Element 數(shù)組合并之后,再將其賦值給 PathClassLoader 的 Element 數(shù)組。這樣就將主、從 dex 中類(lèi)的訪(fǎng)問(wèn)方式進(jìn)行了統(tǒng)一,所以也稱(chēng)為 dex 的注入。
那么什么時(shí)候加載從 dex 呢?這個(gè)問(wèn)題也就是從 dex 的加載時(shí)機(jī)。
如 果是啟動(dòng)時(shí)同步加載,一般可以在 Application 的 onCreate 或 attachBaseContext 中執(zhí)行加載,兩者區(qū)別不大。不過(guò),由于 Application 的 onCreate 調(diào)用是在 ContentProvider 的 OnCreate 調(diào)用之后,而 attachBaseContext 的調(diào)用是在 ContentProvider 的 OnCreate 調(diào)用之前,所以當(dāng) app 有注冊(cè) ContentProvider 的時(shí)候,就必須在 attachBaseContext 中加載從 dex。
如果是按需加載,則在代碼充分解耦后,只要在從 dex 中的代碼調(diào)用之前執(zhí)行加載,都是可以的。
3.安裝運(yùn)行
Dex 拆分腳本和加載代碼都完成了,打一個(gè)包,然后在 Android 2.3 系統(tǒng)的手機(jī)上安裝運(yùn)行試試吧。一切順利,終于出現(xiàn)了久違的閃屏頁(yè)!
4.小結(jié)
上 面就是項(xiàng)目 dex 分包方案的研究經(jīng)過(guò),主要是把 Google 的方案研究清楚以后,又參考了網(wǎng)上的一些開(kāi)源代碼,從而實(shí)現(xiàn)了自己的 DEX 自動(dòng)拆包和動(dòng)態(tài)加載方案。在我們的方案中,可以通過(guò)腳本工具來(lái)完全定制拆分過(guò)程和主、從 dex 文件內(nèi)容,在運(yùn)行時(shí)也能比較自由、靈活的動(dòng)態(tài)加載從 dex。
四、性能影響
Dex 分包后,如果是啟動(dòng)時(shí)同步加載,對(duì)應(yīng)用的啟動(dòng)速度會(huì)有一定的影響,但是主要影響的是安裝后***啟動(dòng)。這是因?yàn)榘惭b后***啟動(dòng)時(shí),Android 系統(tǒng)會(huì)對(duì)加載的從 dex 做 Dexopt 并生成 ODEX,而 Dexopt 是比較耗時(shí)的操作,所以對(duì)安裝后***啟動(dòng)速度影響較大。在非安裝后***啟動(dòng)時(shí),應(yīng)用只需加載 ODEX,這個(gè)過(guò)程速度很快,對(duì)啟動(dòng)速度影響不大。同時(shí),從 dex 的大小也直接影響啟動(dòng)速度,即從dex 越小則啟動(dòng)越快。
目前項(xiàng)目的從 dex 的原始大小在 1M 左右。經(jīng)過(guò)測(cè)試,安裝后***啟動(dòng)時(shí),在 GT-I8160(Android 2.3) 上加載耗時(shí)大約 1200ms,在 N i9250(Android 4.3) 上加載耗時(shí)大約 1000ms;非安裝后***啟動(dòng)時(shí),在這兩臺(tái)測(cè)試手機(jī)上的加載速度分別為約 10ms 和 4ms。
五、后續(xù)
分包方案落地后,我們又解決了覆蓋安裝和 MD5 校驗(yàn)的問(wèn)題。不過(guò)后續(xù)還有不少可優(yōu)化的點(diǎn)如下:
(1) 應(yīng)用啟動(dòng)性能的優(yōu)化。如添加啟動(dòng)頁(yè)、提前做 DexOpt 等;
(2) 編譯腳本性能優(yōu)化。由于分包是一個(gè)比較復(fù)雜和耗時(shí)的過(guò)程,開(kāi)始時(shí)分包腳本的性能并不理想,后來(lái)經(jīng)過(guò)我們兩次優(yōu)化,將打***程中的分包時(shí)間從7分多鐘優(yōu)化到10秒以?xún)?nèi);
(3) 研究未來(lái)可能的按需加載或異步加載從 dex 的問(wèn)題。
騰訊Bugly簡(jiǎn)介
Bugly 是騰訊內(nèi)部產(chǎn)品質(zhì)量監(jiān)控平臺(tái)的外發(fā)版本,支持iOS和Android兩大主流平臺(tái),其主要功能是App發(fā)布以后,對(duì)用戶(hù)側(cè)發(fā)生的crash以及卡頓現(xiàn)象進(jìn) 行監(jiān)控并上報(bào),讓開(kāi)發(fā)同學(xué)可以***時(shí)間了解到app的質(zhì)量情況,及時(shí)修改。目前騰訊內(nèi)部所有的產(chǎn)品,均在使用其進(jìn)行線(xiàn)上產(chǎn)品的崩潰監(jiān)控。
騰 訊內(nèi)部團(tuán)隊(duì)4年打磨,目前騰訊內(nèi)部所有的產(chǎn)品都在使用,基本覆蓋了中國(guó)市場(chǎng)的移動(dòng)設(shè)備以及網(wǎng)絡(luò)環(huán)境,可靠性有保證。使用Bugly,你就使用了和手機(jī) QQ、QQ空間、手機(jī)管家相同的質(zhì)量保障手段,Bugly會(huì)持續(xù)對(duì)產(chǎn)品進(jìn)行優(yōu)化打磨,在服務(wù)好內(nèi)部團(tuán)隊(duì)的同時(shí),幫助更多的開(kāi)發(fā)者。