熱修復(fù)兢仰,即熱加載阳似。是指在一個(gè)App正常打開(kāi)之后粗卜,從外部加載本不屬于它的一些資源并加以運(yùn)用的功能。以下demo只是基于熱修復(fù)的原理做到了初步實(shí)現(xiàn)瘤睹,另一個(gè)關(guān)鍵點(diǎn) dex插樁 還有待下一篇文章詳細(xì)講解,因此這一版的demo并不能投入實(shí)際的商業(yè)化運(yùn)用
熱修復(fù)的實(shí)現(xiàn)的途徑有很多種答倡,其中一種是通過(guò)反射來(lái)拿到當(dāng)前應(yīng)用程序的 ClassLoader 中的成員變量:pathList 所指向的實(shí)例轰传。然后再通過(guò)反射這個(gè)實(shí)例,拿到其內(nèi)部成員變量:dexElements數(shù)組 的值瘪撇。由于安卓系統(tǒng)在加載一個(gè)類之前會(huì)從前往后地去遍歷這個(gè) dexElements數(shù)組获茬,尋找當(dāng)前需要加載的 .class 文件。因此倔既,只要我們將外部下載的 apk包 或者是 .dex 文件路徑生成的對(duì)象插入到 dexElements數(shù)組 的最前方恕曲,就可以達(dá)到頂替數(shù)組后面原有的同名 .class 文件的目的,從而實(shí)現(xiàn)外部熱修復(fù)渤涌。示意圖如下(patch.dex就是外部加載進(jìn)來(lái)的修復(fù)包):
因此,在整個(gè)熱加載功能實(shí)現(xiàn)過(guò)程中(包括插件化和so庫(kù)動(dòng)態(tài)加載):Classloader -> 內(nèi)部DexPathList類型的成員變量 -> DexPathList中的成員變量Elements數(shù)組的更改吊履,是整個(gè)功能中比較核心的一環(huán)腾窝,運(yùn)用類加載的實(shí)現(xiàn)方式完成熱修復(fù)的框架,源碼中一定有反射并修改elements數(shù)組的相關(guān)邏輯托慨。
根據(jù)以上提到的思路,我們來(lái)看一下核心的相關(guān)代碼:
拿到 ClassLoader 和 修復(fù)包的存儲(chǔ)路徑 之后奸例,就可以開(kāi)始準(zhǔn)備做 java反射了,這一塊的邏輯我封裝到了 installSecondaryDexes 方法中
根據(jù)不同的系統(tǒng)SDK版本執(zhí)行不同的反射邏輯(我們以 api19 及其以上的代碼為例)
接下來(lái)獲取到 dexElements 數(shù)組炼杖,準(zhǔn)備在數(shù)組第一位插入外部載入的修復(fù)包路徑
由上圖可見(jiàn)罩扇,截圖中的最后一個(gè)方法 makeDexElements 就是插入外部修復(fù)包路徑的封裝方法,這個(gè)方法的第三個(gè)實(shí)參又是一個(gè)方法,這個(gè)方法是根據(jù)不同的手機(jī)SDK版本氯材,將修復(fù)包的路徑封裝成不同類型的對(duì)象組成的數(shù)組,這個(gè)數(shù)組最后會(huì)被插入到 dexElements 數(shù)組當(dāng)中。經(jīng)過(guò)了 expandFieldArray 這個(gè)方法的執(zhí)行之后,原先存儲(chǔ) 外部修復(fù)包 的路徑下就多出了一個(gè) .dex 文件,或者 .odex 和 .vdex 文件。makeDexElements 方法的代碼如下:
我們?cè)倩氐酵鈱拥?expandFieldArray 方法澎埠,其內(nèi)部的邏輯是這樣的:首先通過(guò)反射拿到dexElements的取值,然后將上圖方法獲取到的 object[] 插入到數(shù)組的最前面诉植。這個(gè)被插入的 object[] 數(shù)組就是外部修復(fù)包存儲(chǔ)路徑集合編譯后形成的隊(duì)列啊犬,也就是外部修復(fù)包的資源和 .class 隊(duì)列
以上步驟完成之后,當(dāng)前App的 dexElements 的狀態(tài)就變成了這樣壁查,理論上是可以頂替原包的同名 .class 文件了:
接下來(lái)我們做一些外部的封裝觉至,比如說(shuō)斷點(diǎn)續(xù)傳下載外部修復(fù)包,以及在Splash頁(yè)面上做熱修復(fù)準(zhǔn)備處理等等睡腿,original包和fix包的代碼都在下面的git鏈接上了:
https://github.com/liuchenguangqnm/hot_fix_example
然后我們?cè)僭趥z包的MainActivity上分別寫(xiě)上不同的吐司顯示準(zhǔn)備測(cè)試:
然后找到 original_package 的 Appconfig 類语御,配置好修復(fù)包的下載地址,以及下載到本地的存儲(chǔ)文件名
最后就可以安裝看效果了:
首先第一次打開(kāi)席怪,因?yàn)樾迯?fù)包需要下載時(shí)間应闯,所以第一次打開(kāi)App或者斷網(wǎng)的時(shí)候修復(fù)包是沒(méi)有插入完成的,所以此時(shí)點(diǎn)擊按鈕提示如下:
等到第一次的修復(fù)包插入完成之后挂捻,關(guān)閉MainActivity碉纺,再次打開(kāi),點(diǎn)擊測(cè)試按鈕细层,吐司顯示如下:
以上代碼經(jīng)本人測(cè)試惜辑,可以順利在安卓各個(gè)模擬器和小米4上面跑通。由于開(kāi)篇的時(shí)候我就說(shuō)過(guò)了疫赎,此版本demo由于沒(méi)有做 dex插樁盛撑,因此不能投入商業(yè)化使用,所以捧搞,以上的demo在華為手機(jī)上是跑不通的抵卫。
那么什么是dex插樁呢?
我們首先圍繞上面講到的問(wèn)題出發(fā):為什么有的手機(jī)按照demo上的代碼運(yùn)行可以正常走通胎撇,有些就走不通介粘,難道有的手機(jī)加載類的時(shí)候不是通過(guò)遍歷 dexElements 數(shù)組獲得 .class 文件的嗎?當(dāng)然這是不可能的晚树。以上demo之所以走不通姻采,是因?yàn)樾掳姹镜氖謾C(jī)SDK為了提高App的啟動(dòng)速度,已經(jīng)在我們安裝應(yīng)用的時(shí)候預(yù)先加載了安裝包里的所有 .class 文件的索引爵憎,有了這個(gè)索引慨亲,我們?cè)俅蜷_(kāi)App的時(shí)候,系統(tǒng)就再不用去 遍歷DexElements數(shù)組 尋找對(duì)應(yīng)的 .class 文件了宝鼓。
因?yàn)槊看渭虞d這個(gè)類刑棵,新安卓版本的系統(tǒng)都不用再去遍歷 DexElements數(shù)組 了,因此我們?cè)?App啟動(dòng)之后愚铡,再去對(duì) DexElements數(shù)組 的內(nèi)容進(jìn)行操作蛉签,其實(shí)都是無(wú)意義的,因?yàn)橄到y(tǒng)根本不會(huì)再次遍歷它了。而解決這個(gè)問(wèn)題的途徑之一就是 dex插樁碍舍!
首先柠座,我們要清楚的是,系統(tǒng)在安裝了一個(gè)新的App之后乒验,首先會(huì)查看這個(gè) App 里面有多少 .dex 文件愚隧。如果一個(gè) .class 文件里面使用到的所有類,都在一個(gè) .dex 文件之中存放的話锻全,那么系統(tǒng)就會(huì)給這個(gè)類打上一個(gè) CLASS_ISPREVERIFIED 標(biāo)記狂塘,有了這個(gè)標(biāo)記,下次App如果要再加載這個(gè)類的時(shí)候鳄厌,就不會(huì)再次遍歷 DexElements 數(shù)組了荞胡;如果一個(gè) .class 文件里面使用到的所有類了嚎,分別在兩個(gè) .dex 文件之中的話泪漂,那么為了避免在類運(yùn)行過(guò)程中,因?yàn)槠渲幸粋€(gè) .dex 文件的缺失而導(dǎo)致異常歪泳,此時(shí)這個(gè)類不會(huì)被打上 CLASS_ISPREVERIFIED 標(biāo)記萝勤,每次我們要加載它的時(shí)候,系統(tǒng)還是要遍歷一次 dexElements 數(shù)組敌卓,確保所有的 .dex 文件都是全乎的。
因此,我們需要實(shí)現(xiàn)的就是,在安卓打包的時(shí)候级遭,給所有有可能出bug的類的構(gòu)造方法里面都插入一行初始化其它 .dex 文件中的類型的代碼掠兄,只要有了這行代碼侈贷,這個(gè)類在每次加載之前就必須要重新過(guò)一遍 dexElements 數(shù)組,我們的修復(fù)包就可以趁著這個(gè)時(shí)機(jī),頂?shù)舸嬖赽ug的類燃异。這種在打包時(shí)修改 .class 文件內(nèi)容的技術(shù)鳄逾,就叫做 dex插樁
由于時(shí)間和精力有限,再加上dex插樁技術(shù)本人也需要花更多的時(shí)間去測(cè)試和研究苦锨,因此我會(huì)在下一篇博客繼續(xù)完善上面分享過(guò)的demo
以上氏仗,本篇完結(jié)