09.源碼閱讀(從Android源碼角度入手自己實現熱修復)

首先我們要知道Activity是如何啟動的,在文章http://www.lxweimin.com/p/bd5208574430中我們已經看了Activity啟動的源碼,http://www.lxweimin.com/p/40f436881390 ClassLoader加載類的源碼,這里再簡單回顧一下

Activity加載流程.png

看了Activity的啟動流程和ClassLoader加載類的方式之后,我們知道,當一個Activity要啟動的時候,最終其實是通過從BaseDexClassLoader中的一個DexPathList類中的dexElements數組中取出來,反射得到對象返回的

那么當開發中出現了問題,比如一個Activity中發生了bug,我們要緊急修復這個問題,同時又不需要應用重啟,該怎么做呢,熱修復的解決方案有幾個可選的阿里 AndFix(不再維護了,并且不支持8.0及以上,不建議使用),騰訊Tinker(需要重啟才能生效)。不過今天我們不使用第三方的實現,只從源碼方面找答案

關鍵點就在于dexElements這個數組,經過我們的分析,我們已經知道,所有的類的dexFile文件在應用啟動時就已經被保存的這個數組中了,包括發生了錯誤的文件類,當類被加載的時候,會從前往后遍歷這個數組,找到對應的class然后反射得到對象,那么我們的解決辦法是,能否將正確的文件類插入到這個數組中,當這個類啟動的時候,先加載到我們修復的正確的dexFile文件,從而達到修復的目的,要使用的技術就是反射

實現原理借鑒了騰訊的Tinker,不過Tinker核心思想是利用DexDiff算法對比差異生成Patch補丁包,將生成的差異dex文件插入dexElements,而我們做的卻少了生成差分包的過程,是將整個dex文件插入替換,Tinker原理圖如下:

20170630144819943.png

我們當前要實現的思路就是現上有一個發生bug的app有待修復,我們在線下生成一個修復后的apk,將其后綴改為.zip,然后解壓打開,得到里邊的classes.dex文件,客戶端下載這個修復的dex到本地,然后將這個dex插入本地apk的dex數組中實現修復bug的功能。

構造方法執行,初始化dex文件的存儲位置

public FixDexManager(Context context) {
        this.mContext = context;
        //獲取系統能夠訪問的dex目錄
        this.mDexDir = context.getDir("odex",Context.MODE_PRIVATE);
    }

遍歷所有dex文件,存入集合中,這樣做也是仿照了AndFix的處理方式,目錄下可能存在多個dex文件,所以我們需要將他們都放入集合中,然后開始修復

public void loadFixDex() throws Exception{
        File[] dexFiles = mDexDir.listFiles();
        List<File> fixDexFiles = new ArrayList<>();
        for (File dexFile : dexFiles) {
            if (dexFile.getName().endsWith(".dex")){
                fixDexFiles.add(dexFile);
            }
        }
        fixDexFiles(fixDexFiles);
    }

進入fixDexFiles方法
加載到程序已經運行的dexElements數組,這個數組包括存在問題的類,然后通過BaseDexClassLoader加載得到我們修復后的dex文件中的dexElements數組,最終將這個正確的dexElements數組插入到程序已經運行的dexElements中,從而當程序要啟動一個類的時候,會從數組中獲取到正確的類,達到修復bug的目的

private void fixDexFiles(List<File> fixDexFiles) throws Exception{
        //1.先獲取已經運行的dexElement
        ClassLoader applicationClassLoader = mContext.getClassLoader();
        //Element數組對象
        Object applicationDexElements = getDexElementByClassLoader(applicationClassLoader);

        File optimizedDirectory = new File(mDexDir,"odex");
        if (!optimizedDirectory.exists()){
            optimizedDirectory.mkdirs();
        }

        //修復
        for (File fixDexFile : fixDexFiles) {
            //參數:
            // String dexPath,  dex路徑
            // File optimizedDirectory,
            // String librarySearchPath,  so文件位置
            // ClassLoader parent   父classloader
            ClassLoader fixDexClassLoader = new BaseDexClassLoader(
                    fixDexFile.getAbsolutePath(),//dex路徑,必須要在應用目錄下的odex文件中
                    optimizedDirectory,
                    null,
                    applicationClassLoader
            );

            Object fixDexElements = getDexElementByClassLoader(fixDexClassLoader);

            //3.將下載的dex插入到已經運行的dexElement的最前邊,合并
            //applicationClassLoader 數組合并fixDexElements數組
            applicationDexElements = combineArray(fixDexElements, applicationDexElements);

            //把合并的數組注入到原來的類中 applicationClassLoader
            injectDexElements(applicationClassLoader, applicationDexElements);
        }
    }

通過反射再次獲取到dexElements這個數組的Field,和它所在DexPathList的對象,注入即可

    //把dexElements注入到classLoader中
    private void injectDexElements(ClassLoader classLoader, Object dexElements) throws Exception{
        //先獲取pathList
        Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
        pathListField.setAccessible(true);
        Object pathList = pathListField.get(classLoader);

        //獲取pathList中的dexElements
        Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
        dexElementsField.setAccessible(true);

        //利用反射進行注入
        dexElementsField.set(pathList,dexElements);
    }

使用方法:
在Application啟動的時候初始化并調用修復方法,這種實現方式有很大的問題,因為是將修復后的dex插入原來的dex數組,以保證加載類的時候可以加載到正確的類,那么這種情況下,如果當前bug頁面已經加載出來,這時候再通過這種方式注入dex是不會起作用的,必須在啟動bug類之前將dex注入,否則如果不重啟,不會有效果。所以這種方式實現的熱修復必須要重啟生效。

    FixDexManager manager = new FixDexManager(this);
        //加載所有修復的dex包,第一次的時候會從服務器上下載到修復的dex包并放在我們制定的目錄下,
        //第二次進入的時候,直接讀取保存的文件進行修復
        try {
            manager.loadFixDex();

            File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath(),"fix.apatch");
            if (file.exists()){
                try {
                    manager.fixDex(file.getAbsolutePath());
                    Toast.makeText(getApplicationContext(),"修復bug成功",Toast.LENGTH_SHORT).show();
                } catch (Exception e) {
                    Toast.makeText(getApplicationContext(),"修復bug失敗",Toast.LENGTH_SHORT).show();
                    e.printStackTrace();
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

總結一下:
通過這種方式實現熱修復,需要重啟app,它的缺點也很明顯,由于是將整個dex文件注入到原來dex數組中,會使app內存占用增大一倍左右,我這邊親測,原本占空間10.8M的app,注入新的dex后,內存占用變成了19.8M,可以考慮分包的方案解決這個問題,減少體積。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容