Android熱修復(Hot Fix)案例全剖析(二)

    在上篇博客中,我們初步瞭解了Android熱修復的基本流程,具體可以看我的博客Android熱修復(Hot Fix)案例全剖析(一),那麼本篇博客,我將爲大家全面剖析Android熱修復的實現案例。

1.將下載的修復補丁拷貝到應用的內部緩存目錄中

    在上一篇文章中,我們已經生成了用於修復Bug的classes2.dex補丁包,通常我們會在APP後臺子線程中自動調用熱修復接口,並下載修復補丁,這裏爲了方便演示,我們把已經下載好的dex補丁文件放到SD卡中,然後將下載的修復補丁拷貝到應用的內部緩存目錄中cacheDir,之所以這樣做是因爲下一步我們需要使用類加載器ClassLoader在內部緩存中加載classese.dex包。下面是我寫的一個將classes2.dex包拷貝到內部緩存目錄中的方法。

/**
 * 修復方法
 */
private void castielFixMethod() {
        // 創建一個內部緩存目錄,把我們SD卡中的"classes2.dex"文件拷貝到內部緩存目錄中cache
        File fileSDir = getDir(MyConstants.DEX_DIR, Context.MODE_PRIVATE);
        String name = "classes2.dex";
        String filePath = fileSDir.getAbsolutePath() + File.separator + name;
        File file = new File(filePath);
        if (file.exists()) {// 判斷是否已經存在dex文件
            Log.i("WY", "已經存在dex文件");
            file.delete();
        }
        // 通過IO流將dex文件寫到我們的緩存目錄中去
        InputStream is = null;
        FileOutputStream fos = null;
        // 版權所有,未經許可請勿轉載:猴子搬來的救兵http://blog.csdn.net/mynameishuangshuai
        try {
            is = new FileInputStream(Environment.getExternalStorageDirectory());
            fos = new FileOutputStream(filePath);
            int len = 0;
            byte[] buffer = new byte[1024];
            while ((len = is.read(buffer)) != -1) {
                fos.write(buffer, 0, len);
            }
            File f = new File(filePath);
            Log.i("WY", "filePath:" + f.getAbsolutePath());
            if (f.exists()) {
                Toast.makeText(this, "新的dex文件已經覆蓋", Toast.LENGTH_LONG).show();
            }
            // 動態加載修復dex包 
            FixDexUtils.loadFixedDex(this);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                fos.close();
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

2.實現熱修復工具類

這裏首先給大家普及一下類加載的原理:
    在Android系統啓動的時候會創建一個Boot類型的ClassLoader實例,用於加載一些系統Framework層級需要的類。由於Android應用裏也需要用到一些系統的類,所以APP啓動的時候也會把這個Boot類型的ClassLoader傳進來。此外,APP也有自己的類,這些類保存在APK的dex文件裏面,所以APP啓動的時候,也會創建一個自己的ClassLoader實例,用於加載自己dex文件中的類。
    ClassLoader去加載Dex文件,首先Dex文件是放在/data/apk/packagename~1/base/apk,由於apk是一個類似於壓縮包的東西,Android其實是使用一個優化的臨時緩存目錄optimizeDir(dex),專門把Dex文件解壓進去,這樣以後就從這個臨時緩存目錄中加載,提高效率。
    在1代碼中我們提到了loadFixedDex()方法,便是我們的核心熱修復工具類,我給大傢俱體講一下:
    ClassLoader有一個簡單的實現類-PathClassLoader。該類作爲Android的默認的類加載器,本身繼承自BaseDexClassLoader,BaseDexClassLoader重寫了findClass方法,該方法是ClassLoader的核心。
    每個ClassLoader有一個pathList變量,是標識dex文件的路徑,我們通過該路徑加載dex文件,默認不分包的時候只有一個dex文件,當然谷歌在頂層設計時允許我們有多個dex文件。
    ClassLoader去找optimizeDir(dex)目錄,然後把目錄添加到pathList裏面去,接着去找目錄下面的所有的dex文件,把這些dex文件當做一個數組放到dexElements中去,這樣就可以有多個dex文件。

pathList{
    dexElements{
        [classes.dex,classes2.dex]
    }
}

    ClassLoader每加載一個類,它會先找classes.dex,如果找不到就去classes2.dex中找,如果裏面又一個dex有問題,比如說classes2.dex出問題了,我們就需要弄一個修復的新的classes2.dex文件放到數組中去,替換掉有問題的;但是classes2.dex中可能有多個類,除了有問題的類,也可能有很多正確的類,我們在替換時沒必要把所有的類都替換掉,所以我們只要替換有問題的類。
    爲此,我們可以採用一個策略,把新的替換的dex文件放到數組的最前面,最終數組的形態爲:

[classes2.dex,classes.dex,classes2.dex]

    這裏解釋下,ClassLoader類加載器先加載我們修復的正確的dex文件,然後順序加載數組中其他的dex元素,到了最後加載到舊的classes2.dex元素,由於前面已經加載了更新的classes2.dex(更新的dex文件中只包含修復的class),那麼舊的classes2.dex元素中的有Bug的class就不會再加載,而是隻加載其餘的沒有錯誤的class。
    整個流程其實非常簡單,但是如果我們要實現這個過程卻有個障礙,那就是由於我們的APK程序可能正在運行,谷歌並沒有提供相關的接口方法去實現這一步驟,爲此,我們需要使用反射的手段去實現。
1.首先需要反射ClassLoader類,找到裏面的pathList變量,然後找到dexElements[]數組,該數組在修復之前只有兩個元素,分別是classes.dex和classes2.dex(出錯的),假設值數組1;
2.接着我們要往dexElements[]數組中添加classes2.dex文件。
Android中要想實現加載dex文件,需要使用DexClassLoader類加載classes2.dex(補丁),加載到dexElements[]數組中去,假設值數組2。
3.最後,我們需要把兩個dexElements[]數組合並,作爲一個新數組dexElements[],該數組中包含元素爲classes2.dex(補丁),classes.dex和classes2.dex(出錯的),完成後將數組返回賦值給系統的ClassLoader。

最後貼出熱修復工具類源碼

public class FixDexUtils {

    private static HashSet<File> loadedDex = new HashSet<File>();

    public static void loadFixedDex(Context context) {
        if (context == null) {
            return;
        }
        // 首先拿到緩存目錄
        File fileSDir = context.getDir(MyConstants.DEX_DIR,
                Context.MODE_PRIVATE);
        File[] listFils = fileSDir.listFiles();
        // 遍歷緩存文件
        for (File file : listFils) {
            // 如果文件是以"classes"開始或者以".dex"結尾,說明這是從SDK中拷貝回來的修復包
            if (file.getName().startsWith("classes")
                    || file.getName().endsWith(".dex")) {
                Log.i("WY", "當前dexName:" + file.getName());
                loadedDex.add(file);
            }
        }
        doDexInject(context, fileSDir);
    }

    private static void doDexInject(Context context, File fileDir) {
        if (Build.VERSION.SDK_INT >= 23) {
            Log.i("WY", "Unable to do dex inject on SDK"
                    + Build.VERSION.SDK_INT);
        }
        // .dex 的加載需要一個臨時目錄
        String optimizeDir = fileDir.getAbsolutePath() + File.separator
                + "opt_dex";
        File fopt = new File(optimizeDir);
        if (!fopt.exists())
            fopt.mkdirs();
        try {
            // 根據.dex 文件創建對應的DexClassLoader 類
            for (File file : loadedDex) {// 循環迭代,用於多個修復包同時注入
                DexClassLoader classLoader = new DexClassLoader(
                        file.getAbsolutePath(), fopt.getAbsolutePath(), null,
                        context.getClassLoader());
                // 注入
                inject(classLoader, context);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void inject(DexClassLoader classLoader, Context context) {

        // 獲取到系統的DexClassLoader 類
        PathClassLoader pathLoader = (PathClassLoader) context.getClassLoader();
        try {
            Object dexElements = combineArray(
                    getDexElements(getPathList(classLoader)),
                    getDexElements(getPathList(pathLoader)));
            Object pathList = getPathList(pathLoader);
            setField(pathList, pathList.getClass(), "dexElements", dexElements);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 通過反射獲取DexPathList中dexElements
     */
    private static Object getDexElements(Object paramObject)
            throws IllegalArgumentException, NoSuchFieldException,
            IllegalAccessException {
        return getField(paramObject, paramObject.getClass(), "dexElements");
    }

    /**
     * 通過反射獲取BaseDexClassLoader中的PathList對象
     */
    private static Object getPathList(Object baseDexClassLoader)
            throws IllegalArgumentException, NoSuchFieldException,
            IllegalAccessException, ClassNotFoundException {
        return getField(baseDexClassLoader,
                Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

    /**
     * 通過反射獲取指定字段的值
     */
    private static Object getField(Object obj, Class<?> cl, String field)
            throws NoSuchFieldException, IllegalArgumentException,
            IllegalAccessException {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        return localField.get(obj);
    }

    /**
     * 通過反射設置字段值
     */
    private static void setField(Object obj, Class<?> cl, String field,
            Object value) throws NoSuchFieldException,
            IllegalArgumentException, IllegalAccessException {

        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        localField.set(obj, value);
    }

    /**
     * 合併兩個數組
     */
    private static Object combineArray(Object arrayLhs, Object arrayRhs) {
        Class<?> localClass = arrayLhs.getClass().getComponentType();
        int i = Array.getLength(arrayLhs);
        int j = i + Array.getLength(arrayRhs);
        Object result = Array.newInstance(localClass, j);
        for (int k = 0; k < j; ++k) {
            if (k < i) {
                Array.set(result, k, Array.get(arrayLhs, k));
            } else {
                Array.set(result, k, Array.get(arrayRhs, k - i));
            }
        }
        return result;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章