使用MultiDex的坑以及解決方法

申明. 本文章轉(zhuǎn)載于網(wǎng)絡(luò), 本人僅用于自我學(xué)習(xí)以及需要的朋友參考

一尚洽、遭遇MultiDex

愉快地寫著Android代碼的總悟君往工程里引入了一個(gè)默默無(wú)聞的jar然后Run了一下署尤, 經(jīng)過(guò)漫長(zhǎng)的等待AndroidStudio構(gòu)建失敗了趾访。
于是總悟君帶著疑惑查看錯(cuò)誤信息。

UNEXPECTED TOP-LEVEL EXCEPTION: java.lang.IllegalArgumentException: method ID not in [0, 0xffff]: 65536 
    at com.android.dx.merge.DexMerger$6.updateIndex(DexMerger.java:501) 
    at com.android.dx.merge.DexMerger$IdMerger.mergeSorted(DexMerger.java:276) 
    at com.android.dx.merge.DexMerger.mergeMethodIds(DexMerger.java:490) 
    at com.android.dx.merge.DexMerger.mergeDexes(DexMerger.java:167) 
    at com.android.dx.merge.DexMerger.merge(DexMerger.java:188) 
    at com.android.dx.command.dexer.Main.mergeLibraryDexBuffers(Main.java:439) 
    at com.android.dx.command.dexer.Main.runMonoDex(Main.java:287) 
    at com.android.dx.command.dexer.Main.run(Main.java:230) 
    at com.android.dx.command.dexer.Main.main(Main.java:199) 
    at com.android.dx.command.Main.main(Main.java:103):Derp:dexDerpDebug FAILED

看起來(lái)是:在試圖將 classes和jar塞進(jìn)一個(gè)Dex文件的過(guò)程中產(chǎn)生了錯(cuò)誤仁卷。

早期的Dex文件保存所有classes的方法個(gè)數(shù)的范圍在0~65535之間。業(yè)務(wù)一直在增長(zhǎng),總悟君寫(copy)的代碼越來(lái)越長(zhǎng)引入的庫(kù)越來(lái)越多引谜,超過(guò)這個(gè)范圍只是時(shí)間問(wèn)題。
怎么解擎浴?员咽?太陽(yáng)底下木有新鮮事,淡定先google一發(fā)贮预,找找已經(jīng)踩過(guò)坑的小伙伴贝室。

StackOverflow 的網(wǎng)友們對(duì)該問(wèn)題表示情緒穩(wěn)定契讲,談笑間拋出multiDex。
這是Android官網(wǎng)對(duì)當(dāng)初的短視行為給出的補(bǔ)丁方案档玻。文檔說(shuō)怀泊,Dalvik Executable (DEX)文件的總方法數(shù)限制在65536以內(nèi),其中包括Android framwork method误趴, lib method (后來(lái)總悟君發(fā)現(xiàn)僅僅是Android 自己的框架的方法就已經(jīng)占用了1w多)霹琼,還有你的 code method ,所以請(qǐng)使用MultiDex凉当。 對(duì)于5.0以下版本枣申,請(qǐng)使用multidex support library (這個(gè)是我們的補(bǔ)丁包!build tools 請(qǐng)升級(jí)到21)看杭。而5.0及以上版本忠藤,由于ART模式的存在,app第一次安裝之后會(huì)進(jìn)行一次預(yù)編譯(pre-compilation) 楼雹,如果這時(shí)候發(fā)現(xiàn)了classes(..N).dex文件的存在就會(huì)將他們最終合成為一個(gè).oat的文件模孩,嗯看起來(lái)很厲害的樣子。
同時(shí)Google建議review代碼的直接或者間接依賴贮缅,盡可能減少依賴庫(kù)榨咐,設(shè)置proguard參數(shù)進(jìn)一步優(yōu)化去除無(wú)用的代碼。嗯谴供,這兩個(gè)實(shí)施起來(lái)倒是很簡(jiǎn)單块茁,但是治標(biāo)不治本,躲得過(guò)初一躲不過(guò)十五桂肌。
在Google給出這個(gè)解決方案之前数焊,他們的開(kāi)發(fā)人員先給了一個(gè)簡(jiǎn)陋簡(jiǎn)易版本的multiDex具體參看這里。(懷疑后來(lái)的官方解決方案就有這家伙參與)崎场。簡(jiǎn)單地說(shuō)就是:1.先把你的app 的class 拆分成主次兩個(gè)dex佩耳。2.你的程序運(yùn)行起來(lái)后,自己把第二個(gè)dex給load進(jìn)來(lái)谭跨〔戏撸看就這么簡(jiǎn)單!而且這就是個(gè)動(dòng)態(tài)加載模塊的框架饺蚊! 然而總悟君早已看穿Dalvik VM 這種動(dòng)態(tài)加載dex 的能力歸根結(jié)底還是因?yàn)閖ava 的classloader類加載機(jī)制萍诱。沿著這條道走,Android模塊動(dòng)態(tài)化加載污呼,包括dex級(jí)別和apk級(jí)別的動(dòng)態(tài)化加載裕坊,各種玩法層出不窮。參見(jiàn)這里1燕酷、2籍凝、3周瞎、4、5饵蒂、6声诸。

二、天真的官方補(bǔ)丁方案

還是先解決打包問(wèn)題退盯,回頭再研究那些高深的動(dòng)態(tài)化加載技術(shù)彼乌。偷懶一下,考慮到投入產(chǎn)出比渊迁,決定使用Google官方的multiDex解決慰照。(Google的補(bǔ)丁方案啊,不會(huì)再有坑了吧琉朽?后面才發(fā)現(xiàn)還是太天真)
該方案有兩步:

1毒租、修改gradle腳本來(lái)產(chǎn)生多dex。
2箱叁、修改manifest 使用MulitDexApplication墅垮。

步驟1.在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'
}

步驟2. manifest聲明修改

<?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>

如果有自己的Application,繼承MulitDexApplication耕漱。如果當(dāng)前代碼已經(jīng)繼承自其它Application沒(méi)辦法修改那也行算色,就重寫 Application的attachBaseContext()這個(gè)方法。

@Override
protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    MultiDex.install(this);     

}

run一下孤个,可以了!但是dex過(guò)程好像變慢了沛简。齐鲤。。

文檔還寫明了multiDex support lib 的局限椒楣。瞄一下是什么:
1给郊、在應(yīng)用安裝到手機(jī)上的時(shí)候dex文件的安裝是復(fù)雜的(complex)有可能會(huì)因?yàn)榈诙€(gè)dex文件太大導(dǎo)致ANR。請(qǐng)用proguard優(yōu)化你的代碼捧灰。呵呵
2淆九、使用了mulitDex的App有可能在4.0(api level 14)以前的機(jī)器上無(wú)法啟動(dòng),因?yàn)镈alvik linearAlloc bug(Issue 22586) 毛俏。請(qǐng)多多測(cè)試自祈多福炭庙。用proguard優(yōu)化你的代碼將減少該bug幾率。呵呵
3煌寇、使用了mulitDex的App在runtime期間有可能因?yàn)镈alvik linearAlloc limit (Issue 78035) Crash焕蹄。該內(nèi)存分配限制在 4.0版本被增大,但是5.0以下的機(jī)器上的Apps依然會(huì)存在這個(gè)限制阀溶。
4腻脏、主dex被dalvik虛擬機(jī)執(zhí)行時(shí)候鸦泳,哪些類必須在主dex文件里面這個(gè)問(wèn)題比較復(fù)雜。build tools 可以搞定這個(gè)問(wèn)題永品。但是如果你代碼存在反射和native的調(diào)用也不保證100%正確做鹰。呵呵

感覺(jué)這就是個(gè)坑啊。補(bǔ)丁方案又引入一些問(wèn)題鼎姐。但是插件化方案要求對(duì)現(xiàn)有代碼有比較大的改動(dòng)钾麸,代價(jià)太大,而且動(dòng)態(tài)化加載框架意味著維護(hù)成本更高症见,會(huì)有更多潛在bug喂走。所以先測(cè)試,遇到有問(wèn)題的版本再解決谋作。

三芋肠、啥?dexopt failed遵蚜?

呵呵帖池,部分低端2.3機(jī)型(話說(shuō)2.3版本的android機(jī)有高端機(jī)型么)安裝失敗吭净!INSTALL_FAILED_DEXOPT睡汹。這個(gè)就是前面說(shuō)的Issue 22586問(wèn)題。

apk是一個(gè)zip壓縮包寂殉,dalvik每次加載apk都要從中解壓出class.dex文件囚巴,加載過(guò)程還涉及到dex的classes需要的雜七雜八的依賴庫(kù)的加載,真耗時(shí)間友扰。于是Android決定優(yōu)化一下這個(gè)問(wèn)題彤叉,在app安裝到手機(jī)之后,系統(tǒng)運(yùn)行dexopt程序?qū)ex進(jìn)行優(yōu)化村怪,將dex的依賴庫(kù)文件和一些輔助數(shù)據(jù)打包成odex文件秽浇。存放在cache/dalvik_cache目錄下。保存格式為apk路徑 @ apk名 @ classes.dex甚负。這樣以空間換時(shí)間大大縮短讀取/加載dex文件的過(guò)程柬焕。

那剛才那個(gè)bug是啥問(wèn)題呢,原來(lái)dexopt程序的dalvik分配一塊內(nèi)存來(lái)統(tǒng)計(jì)你的app的dex里面的classes的信息梭域,由于classes太多方法太多超過(guò)這個(gè)linearAlloc 的限制 斑举。那減小dex的大小就可以咯。

gradle腳本如下:

android.applicationVariants.all {
    variant ->
        dex.doFirst{
            dex->
            if (dex.additionalParameters == null) {
                dex.additionalParameters = []
            }
                dex.additionalParameters += '--set-max-idx-number=48000'

       }
}

–set-max-idx-number= <value>用于控制每一個(gè)dex的最大方法個(gè)數(shù)病涨,寫小一點(diǎn)可以產(chǎn)生好幾個(gè)dex懂昂。
踩過(guò)更多坑的FB的工程師表示這個(gè)linearAlloc的限制不僅僅在安裝時(shí)候的dexopt程序里7,還在你的app的dalvik rumtime里。(很顯然啊dvk vm的宿主進(jìn)程fork自于同一個(gè)母體傲璞颉)沸柔。為了表示對(duì)這個(gè)坑的不滿以及對(duì)Google的產(chǎn)品表示遺憾,F(xiàn)B工程師Read The Fucking Source Code找到了一個(gè)hack方案铲敛。這個(gè)linearAlloc的size定義在c層而且是一個(gè)全局變量褐澎,他們通過(guò)對(duì)結(jié)構(gòu)體的size的計(jì)算成功覆蓋了該值的內(nèi)容,這里要特別感謝C語(yǔ)言的指針和內(nèi)存的設(shè)計(jì)伐蒋。C的世界里工三,You Are The King of This World。當(dāng)然實(shí)際情況是大部分用戶用這把利刃割傷了自己先鱼。俭正。。別問(wèn)總悟君誰(shuí)是世界上最好的語(yǔ)言焙畔。掸读。。

為FB的工程師的機(jī)智和務(wù)實(shí)精神點(diǎn)贊宏多!然而總悟君不愿意花那么多精力實(shí)現(xiàn)FB的hack方法儿惫。(dvk虛擬機(jī)c層代碼在2.x 4.x 版本里有變更,找到那個(gè)內(nèi)存地址太難伸但,未必搞得定吧銮搿)我們有偷懶的解決方案,為了避免2.3機(jī)型runtime 的linearAlloclimit ,最好保持每一個(gè)dex體積<4M ,剛才的的value<=48000

好了 現(xiàn)在2.3的機(jī)器可以安裝run起來(lái)了更胖!

四铛铁、ANR的意思就是Application Not Responding

問(wèn)題又來(lái)了!這次不僅僅是2.3 的機(jī)型却妨!還有一些中檔配置的4.x系統(tǒng)的機(jī)型饵逐。問(wèn)題現(xiàn)象是:第一次安裝后,點(diǎn)擊圖標(biāo)管呵,1s梳毙,2s哺窄,3s… 程序沒(méi)有任何反應(yīng)就好像你沒(méi)點(diǎn)圖標(biāo)一樣捐下。

5s過(guò)去。萌业。坷襟。程序ANR!

其實(shí)不僅僅總悟君的App存在這個(gè)問(wèn)題,其他很多App也存在首次安裝運(yùn)行后幾秒都無(wú)任何響應(yīng)的現(xiàn)象或者最后ANR了生年。唯一的例外是美團(tuán)App婴程,點(diǎn)擊圖標(biāo)立馬就出現(xiàn)界面。唉要不就算啦抱婉?反正就一次档叔。桌粉。。不行衙四,這可是產(chǎn)品給用戶的第一印象啊太重要了铃肯,而且美團(tuán)搞得定就說(shuō)明這問(wèn)題有解決方案。

ANR了是不是局限1描述的現(xiàn)象传蹈?押逼?不過(guò)也不重要…因?yàn)镚oogle只是告訴你說(shuō)第二個(gè)dex太大了導(dǎo)致的。并沒(méi)有進(jìn)一步解釋根本原因惦界。怎么辦挑格?Google一發(fā)?搜索點(diǎn)擊圖標(biāo) 然后ANR沾歪?怎么可能有解決方案嘛漂彤。ANR就意味著UI線程被阻塞了,老老實(shí)實(shí)查看log吧瞬逊。

adb logcat -v time > log.txt

于是發(fā)現(xiàn) 是 install dex + dexopt 時(shí)間太長(zhǎng)显歧!

梳理一下流程:
安裝完app點(diǎn)擊圖標(biāo)之后,系統(tǒng)木有發(fā)現(xiàn)對(duì)應(yīng)的process确镊,于是從該apk抽取classes.dex(主dex) 加載士骤,觸發(fā) 一次dexopt。

App 的laucherActivity準(zhǔn)備啟動(dòng) 蕾域,觸發(fā)Application啟動(dòng)拷肌,

Application的 onattach()方法調(diào)用,這時(shí)候MultiDex.install()調(diào)用旨巷,classes2.dex 被install巨缘,再次觸發(fā)dexopt。

然后Applicaition onCreate()執(zhí)行采呐。

然后 launcher Activity真的起來(lái)了若锁。

這些必須在5s內(nèi)完成不然就ANR給你看!

有點(diǎn)棘手斧吐。首先主dex是無(wú)論如何都繞不過(guò)加載和dexopt的又固。如果主dex比較小的話可以節(jié)省時(shí)間。主dex小就意味著后面的dex大啊煤率,MultiDex.install()是在主線程里做的仰冠,總時(shí)間又沒(méi)有實(shí)質(zhì)性改變。install() 能不能放到線程里做暗础洋只?貌似不行。。识虚。如果異步化肢扯,什么時(shí)候install完成都不知道。這時(shí)候如果進(jìn)程需要seconday.dex里的classes信息不就悲劦4浮颓哮?主dex越小這個(gè)錯(cuò)誤幾率就越大拐格。要悲劇啊總悟君。
淡定,這次Google搜索MultiDex.install 奸焙。于是總悟君發(fā)現(xiàn)了美團(tuán)多dex拆包方案掂器。 讀完之后感覺(jué)看到勝利曙光格嘁。美團(tuán)的主要思路是:精簡(jiǎn)主dex+異步加載secondary.dex 这橙。對(duì)異步化執(zhí)行速度的不確定性,他們的解決方案是重寫Instrumentation execStartActivity 方法熬丧,hook跳轉(zhuǎn)Activity的總?cè)肟谧雠袛嗨袼冢绻?dāng)前secondary.dex 還沒(méi)有加載完成,就彈一個(gè)loading Activity等待加載完成析蝴,如果已經(jīng)加載完成那最好不過(guò)了害捕。不錯(cuò),RTFSC果然是王道闷畸。
可以試一試尝盼。

但是有幾個(gè)問(wèn)題需要解決:
1、分析主dex需要的classes這個(gè)腳本比較難寫佑菩。盾沫。。Google文檔說(shuō)過(guò)這個(gè)問(wèn)題比較復(fù)雜殿漠, 而且buildTools 不是已經(jīng)幫我們搞定了嗎赴精?去瞄一下主dex的大小:8M 以及secondary.dex 3M 绞幌。 它是如何工作的蕾哟?文檔說(shuō)dx的時(shí)候,先依據(jù)manifest里注冊(cè)的組件生成一個(gè) main-list莲蜘,然后把這list里的classes所依賴的classes找出來(lái)谭确,把他們打成classes.dex就是主dex。剩下的classes都放clsses2.dex(如果使用參數(shù)限制dex大小的話可能會(huì)有classe3.ex 等等) 菇夸。主dex至少含有main-list 的classes + 直接依賴classes 琼富,使用mini-main-list參數(shù)可以僅僅包含剛才說(shuō)的classes仪吧。

關(guān)于寫分析腳本的思路是:直接使用mini-main-list參數(shù)獲取build目錄下的main-list文件庄新,這樣manifest聲明的類和他們的直接依賴類搞定的了,那后者的直接依賴類怎么解?這些在dvk runtime也是必須的classes择诈。一個(gè)思路是解析class文件獲得該class的依賴類械蹋。還一個(gè)思路是自己使用Dexclassloader 加載dex,然后hook getClass()方法羞芍,調(diào)用一次就記錄一個(gè)哗戈。都挺折騰的。

2荷科、由于歷史原因唯咬,總悟君在維護(hù)的App的manifest注冊(cè)的組件的那些類,承載業(yè)務(wù)太多畏浆,依賴很多三方j(luò)ar胆胰,導(dǎo)致直接依賴類非常多,而且短時(shí)間內(nèi)無(wú)法梳理精簡(jiǎn)刻获,沒(méi)辦法mini化主dex蜀涨。

3、Application的啟動(dòng)入口太多蝎毡。Appication初始化未必是由launcher Activity的啟動(dòng)觸發(fā)厚柳,還有可能是因?yàn)镾ervice ,Receiver 沐兵,ContentProvider 的啟動(dòng)别垮。 靠攔截重寫Instrumentation execStartActivity 解決不了問(wèn)題。要為 Service 扎谎,Receiver 宰闰,ContentProvider 分別寫基類,然后在oncreate()里判斷是否要異步加載secondary.dex簿透。如果需要移袍,彈出Loading Acitvity?用戶看到這個(gè)會(huì)感覺(jué)比較怪異老充。

結(jié)合自身App的實(shí)際情況來(lái)看美團(tuán)的拆包方案雖然很美好然但是不能照搬啊葡盗。果然不能愉快地回家看動(dòng)漫了。

五啡浊、換一種思路

考慮到剛才說(shuō)的2觅够,3原因,先不要急著動(dòng)手寫分析腳本巷嚣〈龋總悟君期望找到更好的方案。問(wèn)題到現(xiàn)在變成了:既希望在Application的attachContext()方法里同步加載secondary.dex廷粒,又不希望卡住UI線程窘拯。如果思路限制在線程異步化上红且,確實(shí)不可能實(shí)現(xiàn)。于是發(fā)現(xiàn)了微信開(kāi)發(fā)團(tuán)隊(duì)的這篇文章涤姊。該文章介紹了關(guān)于這一問(wèn)題 FB/QQ/微信的解決方案暇番。FB的解決思路特別贊,讓Launcher Activity在另外一個(gè)進(jìn)程啟動(dòng)思喊!當(dāng)然這個(gè)Launcher Activity就是用來(lái)load dex 的 壁酬,load完成就啟動(dòng)Main Activity。
微信這篇文章給出了一個(gè)非常重要的觀點(diǎn):安裝完成之后第一次啟動(dòng)時(shí)恨课,是secondary.dex的dexopt花費(fèi)了更多的時(shí)間舆乔。認(rèn)識(shí)到這點(diǎn)非常重要,使得問(wèn)題又轉(zhuǎn)化為:在不阻塞UI線程的前提下剂公,完成dexopt蜕煌,以后都不需要再次dexopt,所以可以在UI線程install dex 了诬留!文章最后給了一個(gè)對(duì)FB方案的改進(jìn)版斜纪。

仔細(xì)讀完感覺(jué)完全可行。

1文兑、對(duì)現(xiàn)有代碼改動(dòng)量最小盒刚。

2、該方案不關(guān)注Application被哪個(gè)組件啟動(dòng)绿贞。Activity 因块,Service ,Receiver 籍铁,ContentProvider 都滿足涡上。(有個(gè)問(wèn)題要說(shuō)明:如細(xì)心網(wǎng)友指出的那樣,新安裝還未啟動(dòng)但是收到Receiver的場(chǎng)景下拒名,會(huì)導(dǎo)致Load界面出現(xiàn)吩愧。這個(gè)場(chǎng)景實(shí)際出現(xiàn)幾率比較少,且僅出現(xiàn)一次增显⊙慵眩可以接受。)

3同云、該方案不限制 Application 糖权,Activity ,Service 炸站,Receiver 星澳,ContentProvider 繼續(xù)新增業(yè)務(wù)。

于是總悟君實(shí)現(xiàn)了這篇文章最后介紹的改進(jìn)版的方法旱易,稍微有一點(diǎn)點(diǎn)擴(kuò)充禁偎。

六腿堤、解決方法

上最終解決問(wèn)題版的代碼!

在Application里面(這里不要再繼承自MultiApplication了届垫,我們要手動(dòng)加載Dex):

import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

public class App extends Application {
    public static final String KEY_DEX2_SHA1 = "dex2-SHA1-Digest";
    @Override
    protected void attachBaseContext(Context base) {
        super .attachBaseContext(base);
        LogUtils.d( "loadDex", "App attachBaseContext ");
        if (!quickStart() && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {//>=5.0的系統(tǒng)默認(rèn)對(duì)dex進(jìn)行oat優(yōu)化
            if (needWait(base)){
                waitForDexopt(base);
            }
            MultiDex.install (this );
        } else {
            return;
        }
    }

    @Override
    public void onCreate() {
        super .onCreate();
        if (quickStart()) {
            return;
        }
        ...
    }

    public boolean quickStart() {
        if (StringUtils.contains( getCurProcessName(this), ":mini")) {
            LogUtils.d( "loadDex", ":mini start!");
            return true;
        }
        return false ;
    }
    //neead wait for dexopt ?
    private boolean needWait(Context context){
        String flag = get2thDexSHA1(context);
        LogUtils.d( "loadDex", "dex2-sha1 "+flag);
        SharedPreferences sp = context.getSharedPreferences(
                PackageUtil.getPackageInfo(context). versionName, MODE_MULTI_PROCESS);
        String saveValue = sp.getString(KEY_DEX2_SHA1, "");
        return !StringUtils.equals(flag,saveValue);
    }
    /**
     * Get classes.dex file signature
     * @param context
     * @return
     */
    private String get2thDexSHA1(Context context) {
        ApplicationInfo ai = context.getApplicationInfo();
        String source = ai.sourceDir;
        try {
            JarFile jar = new JarFile(source);
            Manifest mf = jar.getManifest();
            Map<String, Attributes> map = mf.getEntries();
            Attributes a = map.get("classes2.dex");
            return a.getValue("SHA1-Digest");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null ;
    }
    // optDex finish 
    public void installFinish(Context context){
        SharedPreferences sp = context.getSharedPreferences(
                PackageUtil.getPackageInfo(context).versionName, MODE_MULTI_PROCESS);
        sp.edit().putString(KEY_DEX2_SHA1,get2thDexSHA1(context)).commit();
    }


    public static String getCurProcessName(Context context) {
        try {
            int pid = android.os.Process.myPid();
            ActivityManager mActivityManager = (ActivityManager) context
                    .getSystemService(Context. ACTIVITY_SERVICE);
            for (ActivityManager.RunningAppProcessInfo appProcess : mActivityManager
                    .getRunningAppProcesses()) {
                if (appProcess.pid == pid) {
                    return appProcess. processName;
                }
            }
        } catch (Exception e) {
            // ignore
        }
        return null ;
    }
    public void waitForDexopt(Context base) {
        Intent intent = new Intent();
        ComponentName componentName = new
                ComponentName( "com.zongwu", LoadResActivity.class.getName());
        intent.setComponent(componentName);
        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        base.startActivity(intent);
        long startWait = System.currentTimeMillis ();
        long waitTime = 10 * 1000 ;
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR1 ) {
            waitTime = 20 * 1000 ;//實(shí)測(cè)發(fā)現(xiàn)某些場(chǎng)景下有些2.3版本有可能10s都不能完成optdex
        }
        while (needWait(base)) {
            try {
                long nowWait = System.currentTimeMillis() - startWait;
                LogUtils.d("loadDex" , "wait ms :" + nowWait);
                if (nowWait >= waitTime) {
                    return;
                }
                Thread.sleep(200 );
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

PackageUtil的方法

 public static PackageInfo getPackageInfo(Context context){
        PackageManager pm = context.getPackageManager();
        try {
            return pm.getPackageInfo(context.getPackageName(), 0);
        } catch (PackageManager.NameNotFoundException e) {
            LogUtils.e(e.getLocalizedMessage());
        }
        return  new PackageInfo();
    }

這里使用了classes(N).dex的方式保存了后面的dex而不是像微信目前的做法放到assest文件夾。前面有說(shuō)到ART模式會(huì)將多個(gè)dex優(yōu)化合并成oat文件全释。如果放置在asset里面就沒(méi)有這個(gè)好處了装处。

Launcher Activity 依然是原來(lái)的代碼里的WelcomeActivity。

在Application啟動(dòng)的時(shí)候會(huì)檢測(cè)dexopt是否已經(jīng)完成過(guò)浸船,(檢測(cè)方式是查看sp文件是否有dex文件的SHA1-Digest記錄妄迁,這里要兩個(gè)進(jìn)程讀取該sp,讀取模式是MODE_MULTI_PROCESS)。如果沒(méi)有就啟動(dòng)LoadDexActivity(屬于:mini進(jìn)程) 李命。否則就直接install dex 登淘!對(duì),直接install封字。通過(guò)日志發(fā)現(xiàn)黔州,已經(jīng)dexopt的dex文件再次install的時(shí)候 只耗費(fèi)幾十毫秒。

LoadDexActivity 的邏輯比較簡(jiǎn)單阔籽,啟動(dòng)AsyncTask 來(lái)install dex 這時(shí)候會(huì)觸發(fā)dexopt 流妻。

public class LoadResActivity extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        super .onCreate(savedInstanceState);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN , WindowManager.LayoutParams.FLAG_FULLSCREEN );
        overridePendingTransition(R.anim.null_anim, R.anim.null_anim);
        setContentView(R.layout.layout_load);      
        new LoadDexTask().execute();
    }
    class LoadDexTask extends AsyncTask {
        @Override
        protected Object doInBackground(Object[] params) {
            try {
                MultiDex.install(getApplication());
                LogUtils.d("loadDex" , "install finish" );
                ((App) getApplication()).installFinish(getApplication());
            } catch (Exception e) {
                LogUtils.e("loadDex" , e.getLocalizedMessage());
            }
            return null;
        }
        @Override
        protected void onPostExecute(Object o) {
            LogUtils.d( "loadDex", "get install finish");
            finish();
            System.exit( 0);
        }
    }
    @Override
    public void onBackPressed() {
        //cannot backpress
    }

Manifest.xml 里面

<activity
    android:name= "com.zongwu.LoadResActivity"
    android:launchMode= "singleTask"
    android:process= ":mini"
    android:alwaysRetainTaskState= "false"
    android:excludeFromRecents= "true"
    android:screenOrientation= "portrait" />

<activity
    android:name= "com.zongwu.WelcomeActivity"
    android:launchMode= "singleTop"
    android:screenOrientation= "portrait">
    <intent-filter >
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter >
</activity>

替換Activity默認(rèn)的出現(xiàn)動(dòng)畫 R.anim.null_anim 文件的定義:

<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha
        android:fromAlpha="1.0"
        android:toAlpha="1.0"
        android:duration="550"/>
</set>

微信開(kāi)發(fā)團(tuán)隊(duì)的這篇文章所說(shuō),application啟動(dòng)了LoadDexActivity之后笆制,自身不再是前臺(tái)進(jìn)程所以怎么hold 線程都不會(huì)ANR绅这。

系統(tǒng)何時(shí)會(huì)對(duì)apk進(jìn)行dexopt總悟君其實(shí)并沒(méi)有十分明白。通過(guò)查看安裝運(yùn)行的日志發(fā)現(xiàn)在辆,安裝的時(shí)候packageManagerService會(huì)對(duì)classes.dex 進(jìn)行dexopt 证薇。在調(diào)用MultiDex.install()加載 secondary.dex的時(shí)候,也會(huì)進(jìn)行一次dexopt 匆篓。 這背后的流程到底是怎樣的浑度?dexopt是如何在另外一個(gè)進(jìn)程執(zhí)行的?如果是另外一個(gè)進(jìn)程執(zhí)行為何會(huì)阻塞主app的UI進(jìn)程鸦概? 官方文檔并沒(méi)有詳細(xì)介紹這個(gè)俺泣,那就RTFSC一探究竟吧.

源代碼跟蹤比較長(zhǎng),移步到這里看吧完残。

七伏钠、最終章碎碎念

  • MultiDex的問(wèn)題難點(diǎn)在:要持續(xù)解決好幾個(gè)bug才能最終解決問(wèn)題。進(jìn)一步的谨设,想要仔細(xì)分辨且解決這些bug熟掂,就必須持續(xù)探索一些關(guān)聯(lián)性的概念和原理
  • 耗費(fèi)了這么多時(shí)間來(lái)解決了Android系統(tǒng)的缺陷是不是有點(diǎn)略傷心。這不應(yīng)該是Google給出一個(gè)比較徹底的解決方案嗎扎拣?
  • FB的工程師們腦洞好大赴肚。思考問(wèn)題的方式很值得借鑒素跺。
  • 微信團(tuán)隊(duì)的文章提到逆向了不少App。哈誉券!總悟君感覺(jué)增長(zhǎng)知識(shí)拓寬視野的新技能加強(qiáng)指厌。
  • RTFSC是王道。
  • 在查看log的過(guò)程中發(fā)現(xiàn)一個(gè)比較有趣的現(xiàn)象踊跟。在App的secondary.dex加載之前居然先加載了某數(shù)字公司的dex踩验!(手機(jī)沒(méi)有root但是安裝了xx手機(jī)助手)再加上之前看到的錯(cuò)誤堆棧里Android framework的調(diào)用堆棧之間也赫然有他們的代碼∩堂担總悟君惡意猜測(cè)該app利用了某種手段進(jìn)行了提權(quán)箕憾,hook了系統(tǒng)框架代碼,將自己的代碼注入到了每一個(gè)應(yīng)用app的進(jìn)程里拳昌。嗯袭异。。炬藤。有趣御铃。。沈矿。
嗯今晚已經(jīng)沒(méi)有時(shí)間看動(dòng)漫了畅买。。细睡。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末谷羞,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子溜徙,更是在濱河造成了極大的恐慌湃缎,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蠢壹,死亡現(xiàn)場(chǎng)離奇詭異嗓违,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)图贸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門蹂季,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人疏日,你說(shuō)我怎么就攤上這事偿洁。” “怎么了沟优?”我有些...
    開(kāi)封第一講書人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵涕滋,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我挠阁,道長(zhǎng)宾肺,這世上最難降的妖魔是什么溯饵? 我笑而不...
    開(kāi)封第一講書人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮锨用,結(jié)果婚禮上丰刊,老公的妹妹穿的比我還像新娘。我一直安慰自己增拥,他們只是感情好啄巧,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著跪者,像睡著了一般棵帽。 火紅的嫁衣襯著肌膚如雪熄求。 梳的紋絲不亂的頭發(fā)上渣玲,一...
    開(kāi)封第一講書人閱讀 51,115評(píng)論 1 296
  • 那天,我揣著相機(jī)與錄音弟晚,去河邊找鬼忘衍。 笑死,一個(gè)胖子當(dāng)著我的面吹牛卿城,可吹牛的內(nèi)容都是我干的枚钓。 我是一名探鬼主播,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼瑟押,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼搀捷!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起多望,我...
    開(kāi)封第一講書人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤嫩舟,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后怀偷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體家厌,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年椎工,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了饭于。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡维蒙,死狀恐怖掰吕,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情颅痊,我是刑警寧澤畴栖,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站八千,受9級(jí)特大地震影響吗讶,放射性物質(zhì)發(fā)生泄漏燎猛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一照皆、第九天 我趴在偏房一處隱蔽的房頂上張望重绷。 院中可真熱鬧,春花似錦膜毁、人聲如沸昭卓。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)候醒。三九已至,卻和暖如春杂瘸,著一層夾襖步出監(jiān)牢的瞬間倒淫,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工败玉, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留敌土,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓运翼,卻偏偏與公主長(zhǎng)得像返干,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子血淌,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容