實(shí)現(xiàn)一個(gè)簡(jiǎn)單的代碼熱修復(fù)

剛開(kāi)始聽(tīng)說(shuō)熱修復(fù)的時(shí)候,我的臉上充滿著不可思議,只覺(jué)得這是一個(gè)黑科技。
但是當(dāng)我逐漸深入了解之后,發(fā)現(xiàn)這確實(shí)是一門(mén)復(fù)雜的知識(shí),但卻不是那么高不可攀且難懂的東西。
熱修復(fù)的實(shí)現(xiàn)種類繁多,各種框架也層出不窮,不是一篇文章就能講清楚的。

所以這篇文章的目的很簡(jiǎn)單,就是帶你實(shí)現(xiàn)一個(gè)簡(jiǎn)單的代碼熱修復(fù),親身感受一下熱修復(fù)的神奇。

首先熱修復(fù)指的是三塊:代碼,資源和so庫(kù)。
對(duì)這三塊的修復(fù)有分別對(duì)應(yīng)的方法,而我們這篇文章的關(guān)注點(diǎn)只在 代碼熱修復(fù) 上。

那么代碼熱修復(fù)的方式大致分為兩種:
1.底層替換方式,可立即生效。
2.類加載方式,需重啟才能生效。

感興趣的朋友可以閱讀:《深入探索Android熱修復(fù)技術(shù)原理》 這本書(shū),里面有對(duì)各種技術(shù)的詳細(xì)介紹。

這篇文章選擇的修復(fù)方式是 類加載方式,因?yàn)檫@個(gè)方案是在Java層實(shí)現(xiàn)的,加上核心邏輯非常簡(jiǎn)單,所以便于我們的理解。

為了更直觀的講述熱修復(fù)的實(shí)現(xiàn),我們先構(gòu)建一個(gè)具體的場(chǎng)景:
假設(shè)你有個(gè)頁(yè)面,頁(yè)面中有一個(gè)按鈕,點(diǎn)擊這個(gè)按鈕,彈出一個(gè)Toast,內(nèi)容為 “我是一個(gè)BUG” 。
效果如下:


image.png

現(xiàn)在假設(shè)我們修復(fù)了這個(gè)BUG,那么按照正常的流程,我們需要打包然后發(fā)布。用戶那邊的流程則是下載新的安裝包。

雖然是BUG,但是為了這么一行代碼而進(jìn)行打包發(fā)布的操作,顯得過(guò)于笨重了。對(duì)用戶的體驗(yàn)也不好。
于是熱修復(fù)技術(shù)出現(xiàn)了。他可以在不重新安裝的前提下,更新你的代碼。

那么這樣的操作是如何實(shí)現(xiàn)的呢?我們一起來(lái)看看Android加載代碼的流程。

我們?cè)诰帉?xiě)Android應(yīng)用的時(shí)候,代碼是一個(gè)個(gè)的.java文件,他們會(huì)先被編譯成.class文件,再被轉(zhuǎn)換成.dex文件,而Android加載APP所需要的就是dex文件。

我們熱修復(fù)所做的事情就是用新的修復(fù)了bug的dex文件,替換掉舊的dex文件即可。所以我們可以從dex文件的加載機(jī)制入手。

Android加載dex文件的類有DexClassLoader和PathClassLoader兩種,這兩個(gè)類并沒(méi)有什么本質(zhì)區(qū)別,無(wú)非就是構(gòu)造時(shí)所需的參數(shù)不太一樣。他們都會(huì)調(diào)用他們的父類的構(gòu)造方法進(jìn)行構(gòu)建,也就是 BaseDexClassLoader 的構(gòu)造方法。

如果對(duì)這部分代碼感興趣,大家可以上這個(gè)網(wǎng)站查看源碼:Android Code Search

在源碼中我們可以發(fā)現(xiàn),當(dāng)調(diào)用 BaseDexClassLoader 的構(gòu)造函數(shù)的時(shí)候,會(huì)生成 DexPathList ,而DexPathList構(gòu)造函數(shù)里面有個(gè) dexElements 數(shù)組,存放著的就是App所需的Dex文件

當(dāng)App加載dex文件時(shí),就會(huì)從dexElements數(shù)組里順序?qū)ふ摇D敲次覀兤鋵?shí)只要把補(bǔ)丁dex文件放到dexElements數(shù)組的頭部,就可以先一步被加載。由于類加載的雙親委派機(jī)制,已經(jīng)被加載的類不會(huì)被重復(fù)加載。借此我們就實(shí)現(xiàn)了代碼的熱修復(fù)。

思路有了,接下去就是具體的實(shí)現(xiàn),我們按照步驟一個(gè)個(gè)來(lái)。

1.加載補(bǔ)丁dex

BaseDexClassLoader parentClassLoader = (BaseDexClassLoader) context.getClassLoader();
PathClassLoader dexClassLoader = new PathClassLoader( "dex文件路徑" , parentClassLoader );

2.通過(guò)反射得到補(bǔ)丁的dexElements

Field pathListField = Class.forName("dalvik.system.BaseDexClassLoader").getDeclaredField("pathList");
pathListField.setAccessible(true);
Object pathList = pathListField.get(dexClassLoader);
Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true);

Object newDexElements = dexElementsField.get(pathList);

3.通過(guò)反射得到原本的dexElements

Object parentPathList = pathListField.get(parentClassLoader);
Field parentDexElementsField = parentPathList.getClass().getDeclaredField("dexElements");
parentDexElementsField.setAccessible(true);

Object baseDexElements = parentDexElementsField.get(parentPathList);

4.合并兩個(gè)dexElements,并且把補(bǔ)丁的dexElements放在前面

Class<?> localClass = newDexElements.getClass().getComponentType();
int firstArrayLength = Array.getLength(newDexElements);
int allLength = firstArrayLength + Array.getLength(baseDexElements);

Object allDexElements = Array.newInstance(localClass, allLength);

for (int k = 0; k < allLength; ++k) {
      if (k < firstArrayLength) {
          Array.set(allDexElements, k, Array.get(newDexElements, k));
      } else {
          Array.set(allDexElements, k, Array.get(baseDexElements, k - firstArrayLength));
      }
}

5.用合并完成的dexElements替換掉當(dāng)前的dexElements

Field localField = parentPathList.getClass().getDeclaredField("dexElements");
localField.setAccessible(true);
localField.set(parentPathList, allDexElements);
注意點(diǎn):

1.補(bǔ)丁dex文件如何獲得:
首先我們修復(fù)完代碼上的bug,例如我們上述的例子中,將Toast彈出的提示文案改為 “ 我被修復(fù)啦 ” ,就表示我們已經(jīng)修復(fù)了bug,接下來(lái)我們點(diǎn)擊Android Studio 上 Build 選項(xiàng),點(diǎn)擊 Rebuild Project 。

接下來(lái)我們的左側(cè)欄中會(huì)多出一個(gè) build 文件夾,如圖所示:


image.png

然后我們將修復(fù)了BUG的.class文件連帶著包名拷貝出來(lái),例如我這邊修復(fù)的文件是MainActivity.class,那么我們需要把 com.chentian.androidhotloadtest.bug.MainActivity.class 整個(gè)都拷貝出來(lái)

接下來(lái)我們需要使用 Android SDK 里的 dk 工具,將其轉(zhuǎn)換成dex文件,該工具位于 build-tools里,建議將其添加進(jìn)環(huán)境變量,方便使用:


image.png

那么接下來(lái)我們輸入如下命令:


image.png

該命令會(huì)將MainActivity.class 文件轉(zhuǎn)換成 hot.dex 文件。具體的命令使用方法可以查閱 dex --help 。

在正常的熱修復(fù)中,補(bǔ)丁文件是通過(guò)服務(wù)器下發(fā)的,這里為了方便演示,我們直接把 hot.dex 文件放入手機(jī)的 Download目錄下的hotload文件夾中(注意要獲取存儲(chǔ)權(quán)限),那么我們之前的代碼中的 "dex文件路徑" 就需要就改成:

PathClassLoader dexClassLoader = new PathClassLoader( Environment.getExternalStoragePublicDirectory("Download").getAbsolutePath() + "/hotload/hot.dex" , parentClassLoader );
image.png

2.熱修復(fù)的時(shí)機(jī)
或許你已經(jīng)發(fā)現(xiàn)了,我們這個(gè)方法并不是直接將本地的 dex文件 偷梁換柱,而是讓其先于 舊dex文件 加載,這樣就會(huì)使用 補(bǔ)丁dex 中新的代碼了。

那么我們進(jìn)行修復(fù)的時(shí)機(jī),肯定是要在 舊dex文件中的有問(wèn)題的類 被加載之前。如果有問(wèn)題的類已經(jīng)被加載,那么它將無(wú)法被卸載,哪怕你進(jìn)行了替換,程序使用的依然還是 舊的有問(wèn)題的類。

所以在這里,我們將修復(fù)的時(shí)機(jī)放在Application中,這是App的初始化入口,它肯定會(huì)先于 MainActivity 。

效果

完成以上步驟后,我們來(lái)查看一下效果


image.png

我們的代碼已經(jīng)被修復(fù)啦~

結(jié)束

感謝大家的閱讀,希望和大家一起進(jìn)步,如果有任何疑問(wèn),歡迎在評(píng)論區(qū)留言,謝謝~

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

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