Android基於DexClassLoader實現熱修復

本文簡單介紹 熱修復的一種實現,插件化原理也可以參考。

Android系統中的類加載器

  • Android系統屏蔽了ClassLoader的findClass加載方法,那麼它自己的類加載時通過什麼樣的方式實現的呢?

  • Android系統中有兩個類加載器分別爲PathClassLoader和DexclassLoader。

  • PathClassLoader和DexClassLoader都是繼承與BaseDexClassLoader,BaseDexClassLoader繼承與ClassLoader。

Android爲什麼會將自己的類加載器派生出兩個不同的子類,它們各自有什麼用?

PathClassLoader和DexClassLoader比較

PathClassLoader

public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    public PathClassLoader(String dexPath, String libraryPath,
            ClassLoader parent) {
        super(dexPath, null, libraryPath, parent);
    }
}

DexClassLoader

public class DexClassLoader extends BaseDexClassLoader {
    public DexClassLoader(String dexPath, String optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(dexPath, new File(optimizedDirectory), libraryPath, parent);
    }
}

BaseDexClassLoader的構造函數

    /**
     * Constructs an instance.
     *
     * @param dexPath the list of jar/apk files containing classes and
     * resources, delimited by {@code File.pathSeparator}, which
     * defaults to {@code ":"} on Android
     * @param optimizedDirectory directory where optimized dex files
     * should be written; may be {@code null}
     * @param libraryPath the list of directories containing native
     * libraries, delimited by {@code File.pathSeparator}; may be
     * {@code null}
     * @param parent the parent class loader
     */
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
            String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

optimizedDirectory:制定輸出dex優化後的odex文件,可以爲null
libraryPath:動態庫路徑(將被添加到app動態庫搜索路徑列表中)
parent:制定父類加載器,以保證雙親委派機制從而實現每個類只加載一次。

可以看出 PathClassLoader和DexClassLoader的區別就在於構造函數中optimizedDirectory、libraryPath這兩個參數。PathClassLoader中optimizedDirectory、libraryPath爲null,DexClassLoader中爲new File(optimizedDirectory)、libraryPath。

optimizedDirectory
BaseDexClassLoader的構造函數利用optimizedDirectory創建了一個DexPathList,看看DexPathList中optimizedDirectory:


public DexPathList(ClassLoader definingContext, String dexPath,
        String libraryPath, File optimizedDirectory) {
    /******部分代碼省略******/
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                       suppressedExceptions);
    /******部分代碼省略******/
}
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory,
                                         ArrayList<IOException> suppressedExceptions) {
   /******部分代碼省略******/
    for (File file : files) {
        /******部分代碼省略******/
        if (file.isDirectory()) {
           /******部分代碼省略******/
        } else if (file.isFile()){
            if (name.endsWith(DEX_SUFFIX)) {
                // Raw dex file (not inside a zip/jar).
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException ex) {
                    System.logE("Unable to load dex file: " + file, ex);
                }
            } else {
                zip = file;
                try {
                    dex = loadDexFile(file, optimizedDirectory);
                } catch (IOException suppressed) {
                    suppressedExceptions.add(suppressed);
                }
            }
        } else {
            System.logW("ClassLoader referenced unknown path: " + file);
        }
        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, false, zip, dex));
        }
    }
    return elements.toArray(new Element[elements.size()]);
}

private static DexFile loadDexFile(File file, File optimizedDirectory)
        throws IOException {
    if (optimizedDirectory == null) {
        return new DexFile(file);
    } else {
        String optimizedPath = optimizedPathFor(file, optimizedDirectory);
        return DexFile.loadDex(file.getPath(), optimizedPath, 0);
    }
}

由以上可以得出以下結論

DexClassLoader:能夠加載自定義的jar/apk/dex
PathClassLoader:只能加載系統中已經安裝過的apk
所以Android系統默認的類加載器爲PathClassLoader,而DexClassLoader可以像JVM的ClassLoader一樣提供動態加載。

Android系統中類加載的雙親委派機制

Android5.1源碼中ClassLoader的loadClass方法

protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
        Class<?> clazz = findLoadedClass(className);

        if (clazz == null) {
            ClassNotFoundException suppressed = null;
            try {
                //先讓父類加載器加載
                clazz = parent.loadClass(className, false);
            } catch (ClassNotFoundException e) {
                suppressed = e;
            }
            //當所有父類節點的類加載器都沒有找到該類時,當前加載器調用findClass方法加載。
            if (clazz == null) {
                try {
                    clazz = findClass(className);
                } catch (ClassNotFoundException e) {
                    e.addSuppressed(suppressed);
                    throw e;
                }
            }
        }

想要動態加載類,可以用 自定義ClassLoader和雙親委派機制 中自定義ClassLoader的方法加載自己定義的class文件麼?看看Android源碼中的ClassLoader的findClass方法:
protected Class<?> findClass(String className) throws ClassNotFoundException {
throw new ClassNotFoundException(className);
}
這個方法直接拋出了“ClassNotFoundException”異常,所以在Android中想通過這種方式實現類的加載時不行的。

總結

ClassLoader的loadClass方法保證了雙親委派機。
BaseDexClassLoader提供了兩種派生類使我們可以加載自定義類。

----------------------基於以上介紹下面進入熱修復的內容-------------------------------

修復流程

在這裏插入圖片描述
在這裏插入圖片描述
兩張圖 第一張是修復流程,第二張思路都在圖片中寫的比較詳細了,不再囉嗦,下面程序實現以上功能。

第一步先把修復bug的包 classes2.dex 拷貝到程序的私有目錄下

 /**
     * 修復原理
     * 使用DexClassLoader:能夠加載未安裝的jar/apk/dex
     * DexClassLoader ->DexPathlist->dexElements[]
     * 將修復bug的dex包與dexElement[] 合併成一個新的dex,並把fix.dex 放到 數組的前面
     * 通過反射將 原來的dexElements[]用 合併後的的dexElements[] 替換掉,這樣就可不會加載舊的帶有bug的class文件。
     * @param view
     */
    public void fixBug(View view) {
        //修復bug
        //1.修復包 classes2.dex
        String dexName = "classes2.dex";
        File sourceFile = new File(Environment.getExternalStorageDirectory(),dexName);

        //DexClassLoader指定的應用程序目錄
        File targetFile = new File(getDir("odex", Context.MODE_PRIVATE).getAbsolutePath()+File.separator+dexName);
        if(targetFile.exists()){
            targetFile.delete();
        }
        //拷貝到私有路徑
        FileUtil.copyFile(sourceFile,targetFile);

        FixDexUtil.loadFixedDex(this);

    }

第二部就是進行合併dex數組,將我們的修復後的包classes2.dex,與當前app的 dexElement 進行合併,生成一個新的DexElement。

以下主要基於反射來實現替換出bug的class 。

獲取 DexPathList 中的 DexElement ,然後將合併後的 NewDexElement 替換掉原來的。這樣就達到了修復的目的,這也是插件化的一種實現原理。

     //dex文件需要被寫入的目錄
        String optimizeDir = filesDir.getAbsolutePath()+File.separator+"opt_dex";
        File fileOpt = new File(optimizeDir);
        if(!fileOpt.exists()){
            fileOpt.mkdirs();
        }
        //1.獲得加載應用程序dex的PathClassLoader
        PathClassLoader systemClassLoader = (PathClassLoader) context.getClassLoader();
        for (File dexFile : loadedDex) {
            //DexClassLoader構造參數
            //String dexPath,dex路徑
            // String optimizedDirectory, 制定輸出dex優化後的odex文件,可以爲null
            // String librarySearchPath, so路徑 動態庫路徑(將被添加到app動態庫搜索路徑列表中)
            // ClassLoader parent 制定父類加載器,以保證雙親委派機制從而實現每個類只加載一次。

            DexClassLoader dexClassLoader = new DexClassLoader(
                    dexFile.getAbsolutePath(),
                    optimizeDir,
                    null,
                    systemClassLoader
                    );

            //3.合併dex
            try {
                Object dexObj = ReflectUtil.getPathList(dexClassLoader);
                Object pathObj = ReflectUtil.getPathList(systemClassLoader);
                Object fixDexElements = ReflectUtil.getDexElements(dexObj);
                Object pathDexElements = ReflectUtil.getDexElements(pathObj);
                //合併兩個數組
                Object newDexElements = ReflectUtil.combineArray(fixDexElements,pathDexElements);
                LogUtil.log("combineArray>>>");
                //重新賦值給PathClassLoader 中的exElements數組

                Object pathList = ReflectUtil.getPathList(systemClassLoader);

                ReflectUtil.setField(pathList,pathList.getClass(),"dexElements",newDexElements);
                LogUtil.log("修復完成>>>");
                LogUtil.log("修復完成>>>"+pathList+"   ");
            } catch (Exception e) {
                e.printStackTrace();
                LogUtil.log("doDexInject Exception>>>"+e.toString());
            }

        }

完整的代碼就不貼了,想看的點這兒

可供參考
Android熱修復(二):以DexClassLoader類加載原理編寫demo實現類替換修復
Android類加載之PathClassLoader和DexClassLoader
https://www.jianshu.com/p/197636e75c06
關於安卓簡單補丁的製作和熱修復的代碼實現

歡迎愛學習的小夥伴加羣一起進步:230274309 。 一起分享,一起進步!少划水,多曬乾貨!!歡迎大家!!!(進羣潛水者勿加)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章