八年Android開發架構師,帶你從零開始手擼一個熱修復框架

1.前言

熱修復原理,這個一直是這幾年來很熱門的話題,在項目中使用的話,也基本要麼是阿里系或者騰訊系的開源框架。可是做爲一個光會使用的程序員是遠遠不夠的。這篇文章會從dex分包的緣由,原理,熱修復的由來及原理爲思路,手動寫一個熱修復的框架,這樣感受比光分析原理要更加深記憶。也是一片比較全面的文章。 秉持着一篇博客一個框架的原則,沒有分開,關於熱修復的全部知識點,都匯聚在這篇博客上,可能略長,但願你們可以認真看完。
先看原理,再擼代碼前端

2.什麼是dex分包

先了解下什麼是dex分包,當咱們把一個apk解壓後,咱們會發現有一個classes.dex的文件,它包含了咱們項目中全部的class文件。可是隨着業務愈來愈複雜,方法數也愈來愈多,當方法數超過必定範圍後,就會致使項目編譯失敗。java

由於一個dvm中存儲方法id用的是short類型,因此就致使dex中方法不能超過65535個

那麼如何解決這個問題尼? 那就是dex分包方案。程序員

2.1 分包的原理面試

就是將編譯好的class文件,拆分打包成2個dex,繞過dex方法的限制,運行時,再動態加載第2個dex文件。數組

這樣除了第1個dex文件外(正常apk中存在的惟一的dex文件),其餘的全部dex文件都以資源的形式放到apk裏面,並在Application的onCreate回調中經過系統的ClassLoader加載它們。

值得注意的是,在注入以前就已經引用到的類,則必須放到第一個dex文件中,不然會提示找不到該文件。服務器

接下來咱們就來看看,如何將第2個dex文件注入到系統中。網絡

3.ClassLoader

在Android中,咱們編譯好的class文件,是須要加載到虛擬機纔會被執行的,而這個加載的過程就是經過ClassLoader來完成的。架構

3.1 ClassLoader體系app

看上圖應該也能明白,咱們第二個dex是以資源的形式存在的,因此咱們要用到的classLoader是DexClassLoader。框架

DexClassPath:能夠從一個jar包或者未安裝的apk中加載dex

看下DexClassLoader是怎麼加載class的,這段邏輯是在它的父類BaseDexClassLoader中,咱們先看下這個類的源碼。

public class BaseDexClassLoader extends ClassLoader {

    // 須要加載的dex列表
    private final DexPathList pathList;

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        // 使用pathList對象查找name類
        Class c = pathList.findClass(name, suppressedExceptions);
        return c;
    }
}

這段代碼很簡單的,就是建立了個DexPathList對象,而後調用它的findClass方法,根據類名,尋找該類,那麼咱們看下DexPathList對象,它在DexClassLoader中。

*package*/ final class DexPathList {
    private static final String DEX_SUFFIX = ".dex";
    private static final String JAR_SUFFIX = ".jar";
    private static final String ZIP_SUFFIX = ".zip";
    private static final String APK_SUFFIX = ".apk";

    private final ClassLoader definingContext;

    // ->> 註釋1
    private final Element[] dexElements;

    public Class findClass(String name, List<Throwable> suppressed) {

          // ->> 註釋2
        for (Element element : dexElements) {
            DexFile dex = element.dexFile;

            if (dex != null) {
                Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
                if (clazz != null) {
                    return clazz;
                }
            }
        }
        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }
}
  • 註釋1:一個ClassLoader能夠包含多個dex文件,每一個dex文件是一個Element,多個dex文件排列成一個有序的數組就是dexElements
  • 註釋2:當找類的時候,會按順序遍歷dex文件,而後從當前遍歷的dex文件中找類,若是找到該類則返回,若是找不到從下一個dex文件繼續查找

那麼顯而易見,咱們就能夠經過反射,強行的將一個外部的dex文件添加到此dexElements中,這樣尋找起類來,就也能夠從咱們第2個dex中尋找了,這樣就算是將咱們第2個dex加載進去了。

3.2 總結一下

1. 由於dvm中存儲方法id用的是short類型,因此就致使dex中方法不能超過65535個,因此咱們會將咱們編譯好的class文件,拆分打包成2個dex,繞過dex方法的限制,運行時再加載第2個dex。

2. 經過源碼咱們可知,一個ClassLoader能夠包含多個dex文件,每一個dex文件是一個Element,多個dex文件排列成了一個有序數組dexElements,在項目運行的過程當中,咱們所需用到的class,就是根據遍歷dexElements去尋找的,將咱們只須要將須要加載的dex文件,經過反射加入到dexElements數組中,就能夠完成加載了。

這下知道了dex分包的緣由和原理了吧,那麼思考一個問題,若是在加載的過程當中有2個同樣的class文件,該怎麼辦?

其實從上述的代碼咱們能夠知道,尋找一個class文件時,它會遍歷dexElements數組,先從第一個dex中去尋找,找到就返回,找不到才從下一個dex繼續找,那麼其實就能夠理解成

若是有兩個重複的class,那麼dex1.class會覆蓋dex2.class

看到這個,聰明如個人你,有沒有想到什麼?

咱們若是有個class裏面有bug,咱們只須要提供一個同樣的class,並把它打包成dex,經過反射,放到dexElements最前端,那是否是就加載咱們新的class,以前的有問題的class是否是就被覆蓋了?沒錯,這就是熱修復原理

4.熱修復原理

這塊知識,其實能夠看下安卓App熱補丁動態修復技術介紹,固然懶惰如你懶得看的話,那麼就繼續看我們的。

咱們知道了,若是兩個class相同,那麼在前面的dex中的class,會覆蓋後面dex中與它同樣的class。以下圖:

那麼熱補丁的原理就是,當修改好了一個類的bug後,將這個類打包成dex,好比叫patch.dex,再經過反射,將該dex放置在dexElements的最前面,那麼這個patch中咱們修改的class就覆蓋了以前出現問題的class。以下圖

好了,熱修復的原理你們明白了吧,下面咱們就開始動手寫一下了,好比如何打包dex,如何將dex加載到最前面。

5.擼代碼

源碼很簡單,點擊按鈕蹦出一個toast MainActivity.java

@Override
public void onClick(View v) {

     switch (v.getId()){

        case R.id.btn:

            MyLogic myLogic = new MyLogic();
            Toast.makeText(MainActivity.this, myLogic.toMsg(), Toast.LENGTH_SHORT).show();
            break;
    }
}

public class MyLogic {

    public String toMsg(){

        return "老闆很摳門";
    }
}

只是隨口一說,爽歸爽,可是不能讓老闆知道,老闆用的時候,得給他手機打個補丁。只能讓別人看,不能讓老闆本身看到。

5.1 製做補丁

1.修改源碼:

首先,咱們先將代碼修正過來,將「老闆很摳門」改爲「老闆人真好」,而後從新編譯項目。

2.找到MyLogic.class 文件:
位置以下圖

3.建立文件夾:

路徑和包名同樣,而後將找到的class文件複製進去

4.打jar包:
在外層目錄下,我是在temp裏面建立的包路徑,因此先切換到temp目錄下 cd temp 進入外層目錄後,再執行打包命令:

jar -cvf my.jar com
注: jar命令是在jdk的bin目錄裏面,不要忘記配置環境變量

5. 打dex包:

執行命令
dx --dex --output=my_dex.jar my.jar 以下圖所示

好了,大功告成,將咱們的my_dex.jar 放到sdcard上就好了,通常是放在服務器提供下載,這裏爲了簡單使用。

5.2 加載補丁

還記得上面咱們說的邏輯嗎?(不記得看上面的4)

咳咳,雖然很囉嗦,可是吧,還得說 經過DexClassLoader加載咱們的補丁(my_dex.jar),而後放到dexElements的前面,替換原有的錯誤。

思路

  1. 反射獲取BaseDexClassLoader對象,而後獲取它的成員變量pathList,pathList爲DexClassLoader的內部類,裏面有成員變量dexElements,獲取它。
  2. 建立DexClassLoader對象,加載咱們的補丁文件(my_dex.jar), 並經過反射獲取它父類的pathList對象,依次獲取補丁包加載後生成的dexElements。
  3. 將2個dexElements經過反射合併,生成新的dexElements
  4. 將新生成的dexElements,經過反射,替換掉當前加載的dexElements。

5.3 擼代碼

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();

        // 獲取咱們補丁的路徑
        String dexPath = Environment.getExternalStorageDirectory().getAbsolutePath()+"/my_dex.jar";

        // 加載補丁
        try {
            inject(dexPath);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 加載補丁
     * */
    private void inject(String dexPath) throws Exception{

        // ================= 1.獲取classes的dexElements ===================

        // 反射獲取 BaseDexClassLoader
        Class<?> mBaseDexClassLoader = Class.forName("dalvik.system.BaseDexClassLoader");

        // 反射獲取 BaseDexClassLoader 中的 pathList
        Field pathListField = mBaseDexClassLoader.getDeclaredField("pathList");
        pathListField.setAccessible(true);
        Object pathList = pathListField.get(getClassLoader());

        // 反射獲取 pathList 中的 dexElements
        Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
        dexElementsField.setAccessible(true);
        Object dexElements = dexElementsField.get(pathList); // pathList爲dexClassLoader中的內部類

        // ================= 2.獲取咱們的補丁中的dexElements ===================

        String dexopt = getDir("dexopt", 0).getAbsolutePath();
        DexClassLoader mDexClassLoader = new DexClassLoader(dexPath, dexopt, dexopt, getClassLoader());

        // 反射獲取加載咱們補丁後 dexClassLoader 中的 pathList
        Field myPathListField = mBaseDexClassLoader.getDeclaredField("pathList");
        myPathListField.setAccessible(true);
        Object myPathList = myPathListField.get(mDexClassLoader);

        // 反射獲取 加載咱們補丁後,pathList 中的 dexElements
        Field myDexElementsField = myPathList.getClass().getDeclaredField("dexElements");
        myDexElementsField.setAccessible(true);
        Object myDexElements = myDexElementsField.get(myPathList);

        // ================= 3.合併數組 ===================
        Object newDexElements = mergeArray(myDexElements, dexElements);

        // ================= 4.將合併後的數組賦值給咱們的app的classLoader ===================
        dexElementsField.set(pathList, newDexElements);
    }

    /**
     * 經過反射合併兩個數組
     */
    private Object mergeArray(Object firstArr, Object secondArr) {
        int firstLength = Array.getLength(firstArr);
        int secondLength = Array.getLength(secondArr);
        int length = firstLength + secondLength;

        Class<?> componentType = firstArr.getClass().getComponentType();
        Object newArr = Array.newInstance(componentType, length);
        for (int i = 0; i < length; i++) {
            if (i < firstLength) {
                Array.set(newArr, i, Array.get(firstArr, i));
            } else {
                Array.set(newArr, i, Array.get(secondArr, i - firstLength));
            }
        }
        return newArr;
    }
}

結果

在源碼不變的基礎上,加載補丁前和加載補丁後的對比

6.踩坑

多是SDK比較新的緣故,因此並未發生網絡上提到的CLASS_ISPREVERIFIED問題,簡單說一下這個問題,在class替換加載的過程當中,虛擬機會將dex優化成odex後纔拿去執行。在這個過程當中會對全部class一個校驗。

假設A類在它的static方法,private方法,構造函數,override方法中直接引用到B類。若是A類和B類在同一個dex中,那麼A類就會被打上CLASS_ISPREVERIFIED標記,替換的話,會拋出異常

6.1 解決辦法

這個規則其實也能夠理解成,只要在static方法,構造方法,private方法,override方法中直接引用了其餘dex中的類,那麼這個類就不會被打上CLASS_ISPREVERIFIED標記。

那麼咱們只須要讓全部類都引用其餘dex中的某個類就能夠了

好比說 在全部類的構造函數中插入這行代碼 System.out.println(AntilazyLoad.class); 這樣當安裝apk的時候,classes.dex內的類都會引用一個在不相同dex中的AntilazyLoad類,這樣就防止了類被打上CLASS_ISPREVERIFIED的標誌了,只要沒被打上這個標誌的類均可以進行打補丁操做。

最後

我本身從事 Android 開發,從業這麼久,我也積累了一些珍藏的資料,分享出來,但願能夠幫助到你們提高進階

免費分享2020年Android開發最全新面試題(含答案解析)​

還分享一份由幾位大佬一塊兒收錄整理的Android學習PDF+架構視頻+面試文檔+源碼筆記高級架構技術進階腦圖、Android開發面試專題資料,高級進階架構資料

若是你有須要的話,能夠簡信我【666】我發給你

喜歡本文的話,不妨順手給我點個小贊、評論區留言或者轉發支持一下唄~

相關文章
相關標籤/搜索