Android 手寫熱修復dex

語言: CN / TW / HK

現有的熱修復框架很多,尤以AndFix 和Tinker比較多

具體的實現方式和專案引用可以參考網路上的文章,今天就不談,也不是主要目的

今天就來探討,如何手寫一個熱修復的功能

對於簡單的專案,不想整合其他修復框架的SDK,也不想用第三方平臺,只是緊急修復一些bug 還是挺方便的

言歸正傳,如果一個或多個類出現bug,導致了崩潰或者資料顯示異常,如果修復呢,如果熟悉jvm dalvik 類的載入機制,就會清楚的瞭解 ClassLoader的 雙親委託機制 就可以通過這個

什麼是雙親委託機制

  1. 當前ClassLoader首先從自己已經載入的類中查詢是否此類已經載入,如果已經載入則直接返回原來已經載入的類。 每個類載入器都有自己的載入快取,當一個類被載入了以後就會放入快取,等下次載入的時候就可以直接返回了。
  2. 當前classLoader的快取中沒有找到被載入的類的時候,委託父類載入器去載入,父類載入器採用同樣的策略,首先檢視自己的快取,然後委託父類的父類去載入,一直到bootstrp ClassLoader.
  3. 當所有的父類載入器都沒有載入的時候,再由當前的類載入器載入,並將其放入它自己的快取中,以便下次有載入請求的時候直接返回。

    突破口來了,看1(如果已經載入則直接返回原來已經載入的類) 對於同一個類,如果先載入修復的類,當後續在載入未修復的類的時候,直接返回修復的類,這樣bug不就解決了嗎?

Nice ,多看原始碼和jvm 許多問題可以從framework和底層去解決

話不多說,提出瞭解決方法,下面著手去實現

``` public class InitActivity extends FragmentActivity {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //這裡預設在SD卡根目錄,實際開發過程中可以把dex檔案放在伺服器,在啟動頁下載後加載進來
    //第二次進入的時候可以根據目錄下是否已經下載過,處理,避免重新下載
    //最後根據當前app版本下載不同的修復dex包 等等一系列處理
    String dexFilePath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/fix.dex";
    DexFile dexFile = null;
    try {
        dexFile = DexFile.loadDex(dexFilePath, null, Context.MODE_PRIVATE);
    } catch (IOException e) {
        e.printStackTrace();
    }

    patchDex(dexFile);

    startActivity(new Intent(this, MainActivity.class));
}

/**
 * 修復過程,可以放在啟動頁,這樣在等待的過程中,網路下載修復dex檔案
 *
 * @param dexFile
 */
public void patchDex(DexFile dexFile) {
    if (dexFile == null) return;
    Enumeration<String> enumeration = dexFile.entries();
    String className;
    //遍歷dexFile中的類
    while (enumeration.hasMoreElements()) {
        className = enumeration.nextElement();
        //載入修復後的類,只能修復當前Activity後加載類(可以放入Application中執行)
        dexFile.loadClass(className, getClassLoader());
    }
}

} ``` 方法很簡單在啟動頁,或者Application中提前載入有bug的類

這裡寫的很簡單,只是展示核心程式碼,實際開發過程中,dex包下載的網路請求,據當前app版本下載不同的修復dex,檔案存在的時候可以在Application中先載入一次,啟動頁就不用載入,等等,一系列優化和判斷處理,這裡就不過多說明,具體一些處理看github上的程式碼

ok 程式碼都瞭解了,這個 fix.dex 檔案哪裡來的呢

熟悉Android apk生成的小夥伴都知道了,跳過這個步驟,不懂的小夥伴繼續往下看

上面的InitActivitystartActivity(new Intent(this, MainActivity.class)); 啟動了一個MainActivity 看看我的MainActivity ``` public class MainActivity extends FragmentActivity {

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //0不能做被除數,這裡會報ArithmeticException異常
    Toast.makeText(this, "結果" + 10 / 0, Toast.LENGTH_LONG).show();
}

} 哎呀不小心,寫了一個bug 0 咋能做除數呢,app已經上線了,這裡必崩啊,咋辦 不要急,按照以下步驟: 1. 我們要修復這個類`MainActivity`,先把bug解決 Toast.makeText(this, "結果" + 10 / 2, Toast.LENGTH_LONG).show(); 2. 把修復類生成`.class`檔案(可以先run一次,之後在 build/intermediates/javac/debug/classes/com開的的資料夾,找到生成的class檔案,也可以通過javac 命令列生成,也可以通過右邊的gradle Task生成) ![class 路徑圖](http://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a21ebdbb40da4ec78c54022a291693ff~tplv-k3u1fbpfcp-zoom-1.image) 3. 把修復類`.class`檔案 打包成dex (其他`.class`刪除,只保留修復類) 開啟cmd命令列,輸入下面命令 D:\Android\sdk\build-tools\28.0.3\dx.bat --dex --output C:\Users\pei\Desktop\dx\fix.dex C:\Users\pei\Desktop\dx\ ``D:\Android\sdk為自己sdk目錄28.0.3build-tools版本,可以根據自己已經下載的版本更換 後面兩個目錄分別是生成.dex檔案目錄,和.class`檔案目錄

切記.class檔案的目錄必須是包名一樣的,我的目錄是 C:\Users\pei\Desktop\dx\com\pei\test\MainActivity.class,不然會報 class name does not match path

  1. 這樣dx資料夾下就會生成fix.dex檔案了,把fix.dex放進手機根目錄試試吧

再次開啟App,完美Toast 結果5,完美解決

總結

  1. 修復方法要在bug類之前執行
  2. 適合少量bug,太多bug影響效能
  3. 目前只能修復類,不能修復資原始檔
  4. 目前只能適配單dex的專案,多dex的專案由於當前類和所有的引用類在同一個dex會 當前類被打上CLASS_ISPREVERIFIED標記,被打上這個標記的類不能引用其他dex中的類,否則就會報錯 解決辦法是在構造方法裡引用一個單獨的dex中的類,這樣不符合規則就不會被標記了
「其他文章」