1. 程式人生 > >android原生熱修復流程和原理分析實現

android原生熱修復流程和原理分析實現

首先apk就是一個壓縮檔案,解壓apk檔案的內容如下圖:
這裡寫圖片描述

安卓原生熱修復主要原理圖和流程圖如下,我花了好長時間才繪好,中間改了好幾次,應該來說是很直觀明白的,其中有截取了BaseDexClassLoader的關鍵原始碼,還有DexPathList的原始碼
這裡寫圖片描述


a.現將打包好的dex檔案傳入手機中。
這裡寫圖片描述
b.開始擼程式碼(主介面)
這裡寫圖片描述

public void onClick(View view) {
        switch (view.getId()) {
            case R.id.btn_fix:
                startHotFix();
                break
; case R.id.btn_test: testInject(); break; } }
 //開始熱修復
    private void startHotFix() {
        //pathClassLoader的特點:只能載入本工程目錄下的dex檔案。下面是原始碼的註釋
        //operates on a list of files and directories in the local file system, but does not attempt to
//load classes from the network //建立SD卡目錄存放熱修復的dex File srcDexFolder = initSrcDexFile(); //從SD卡將下載好的dex複製到存放目錄 //建立要在工程目錄存放dex的目錄 File destFile = initDestDexFile(); //開始將剛下載的dex包複製到工程目錄下載面 copyDex2DestFolder(srcDexFolder, destFile); //開始合併dex
try { assembleDex(destFile); } catch (Exception e) { e.printStackTrace(); Log.i(TAG, "startHotFix: =異常=" + e.toString()); } }
 //初始化搬遷目的dex目錄
    private File initDestDexFile(){
        File file = getDir(folderName, MODE_PRIVATE);
        //複製之前,判斷之前熱修復檔案是否存在
        File destFile = new File(file.getAbsolutePath());
        if (!destFile.exists()) {
            destFile.mkdirs();
        } else {
            //清空殘留的dex
            File[] list = destFile.listFiles();
            if (list != null && list.length > 0) {
                for (File fileContent : list) {
                    Log.i(TAG, "startHotFix:要是刪除的:== " + fileContent.getAbsolutePath());
                    fileContent.delete();
                }
            }
        }
        Log.i(TAG, "startHotFix: =path2=" + destFile.getAbsolutePath());
        return destFile;
    }
    //預備熱修復的源目錄
    private File initSrcDexFile() {
        String downLoadedDex = getExternalCacheDir().getAbsolutePath()
                + File.separatorChar + folderName;
        File srcDexFolder = new File(downLoadedDex);
        if (!srcDexFolder.exists()) {
            srcDexFolder.mkdirs();
        } else {
            //測試裡面有幾個剛下載的dex包
            File[] files = srcDexFolder.listFiles();
            if (files != null && files.length > 0) {
                for (File file : files) {
                    Log.i(TAG, "startHotFix: =dexPath=" + file.getAbsolutePath());
                }
            }
        }
        Log.i(TAG, "startHotFix: =path1=" + srcDexFolder.getAbsolutePath());
        return srcDexFolder;
    }
//載入新dex,插入原有dex之前
    private void assembleDex(File destFile) throws Exception {
        //利用應用本身pathClassLoader載入應用的dex
        /**
         * his method should be overridden by class loader implementations that
         * follow the delegation model for loading classes, and will be invoked by
         * 父類委託,所以要強轉
         * pathClassLoader extends BaseDexClassLoader
         */
        PathClassLoader pathClassLoader = (PathClassLoader) getClassLoader();
        File dexOutputDir = getCodeCacheDir();//8.0原始碼推薦
        File[] files = destFile.listFiles();
        //獲取app的libs路徑
        String librarySearchPath = getFileStreamPath("libs").getAbsolutePath();
        if (files != null && files.length > 0) {
            for (File file : files) {
                //一 載入new dex
                DexClassLoader dexClassLoader = new DexClassLoader(
                        file.getAbsolutePath(),  //String dexPath,
                        dexOutputDir.getAbsolutePath(),//String optimizedDirectory,
                        null, //String librarySearchPath,
                        getClassLoader() //ClassLoader parent
                );
                //1.獲取new dex載入中 Pahtlist
                Object newpathList = reflectPathList(dexClassLoader, "pathList");
                //2.獲取 new dex 中的dexElements
                Object newDexElements = reflectDexElements(newpathList, newpathList);

                //二 載入old dex
                //1.獲取 old dex中的PathList
                Object oldPathList = reflectPathList(pathClassLoader, "pathList");
                //2.獲取 old dex中的dexElements
                Object oldDexElements = reflectDexElements(oldPathList, oldPathList);
                //三 將新的dex 插入到舊dexElements中去
                Object fixedElement = injectElements(newDexElements, oldDexElements);
                //四 重新設定pathList的Element[] dexElements
                setFixedElements(reflectPathList(pathClassLoader, "pathList"), fixedElement);
            }
        } else {
            Log.i(TAG, "assembleDex: =為空=");
        }
    }

    //反射得到classLoader中的成員DexPathList pathList
    private Object reflectPathList(Object object, String field) throws Exception {
        //錯誤的反射
            /*BaseDexClassLoader classLoader = (BaseDexClassLoader) getClassLoader();
            Class<? extends BaseDexClassLoader> aClass = classLoader.getClass();
            //pathList在pathClassLoader中為空,在父類BaseDexClassLoader
            Field dexPathList = aClass.getDeclaredField("pathList");
            dexPathList.setAccessible(true);
            Log.i(TAG, "assembleDex: =name="+dexPathList.getName());*/
        //報錯原因如下:(被藏起來了,只反射到子類)
        //java.lang.NoSuchFieldException: No field pathList in class dalvik/system/PathClassLoader;

        //正確的反射
        Class<?> baseClazz = Class.forName("dalvik.system.BaseDexClassLoader");
        Field pathList = baseClazz.getDeclaredField(field);
        pathList.setAccessible(true);
        Log.i(TAG, "assembleDex: =反射正常:=" + pathList.getName());
        return pathList.get(object);
    }

    //反射得到pathList中的成員Element[] dexElements
    private Object reflectDexElements(Object belongWho, Object pathList) throws Exception {
        Class<?> aClass = pathList.getClass();
        Field dexElements = aClass.getDeclaredField("dexElements");
        dexElements.setAccessible(true);
        return dexElements.get(belongWho);
    }

    //兩個資料夾複製
    private void copyDex2DestFolder(@NonNull File sreFolder, @NonNull File destFolder) {
        dexList.clear();
        File[] filesList = sreFolder.listFiles();
        if (filesList != null && filesList.length > 0) {
            for (File file : filesList) {
                //開始複製
                if (file.getName().endsWith(".dex")) {
                    copyFile(file, destFolder);
                }
            }
            //複製完成之後,測試是否複製成功
            testCopyResult(destFolder);
        } else {
            Log.i(TAG, "copyDex2DestFolder: =複製內容為空=");
        }
    }

 //單個檔案複製,一頓流操作
    private void copyFile(@NonNull File file, @NonNull File destFolder) {
        String name = file.getName();
        Log.i(TAG, "copyFile: =name=" + name);
        String destPath = destFolder.getAbsolutePath() + File.separator + name;
        File destFile = new File(destPath);
        try {
            FileOutputStream outs = new FileOutputStream(destFile);
            FileInputStream ins = new FileInputStream(file);
            int len = 0;
            byte[] buffer = new byte[1024];
            while ((len = ins.read(buffer)) != -1) {
                outs.write(buffer, 0, len);
            }
            outs.close();
            ins.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

核心程式碼


    //替換掉pathList原有的陣列
    private void setFixedElements(Object pathList, Object fixedElement) throws Exception {
        Class<?> aClass = pathList.getClass();
        Field dexElements = aClass.getDeclaredField("dexElements");
        dexElements.setAccessible(true);
        dexElements.set(pathList, fixedElement);
    }

    //插入原有陣列中去
    private Object injectElements(@NonNull Object newDexElements, @NonNull Object oldDexElements) {
        //獲取陣列中存放型別
        Class<?> componentType = newDexElements.getClass().getComponentType();
        if (componentType != null) {
            int lengthNew = Array.getLength(newDexElements);
            int lengthOld = Array.getLength(oldDexElements);
            int totalLength = lengthNew + lengthOld;
            Log.i(TAG, "injectElements: =陣列長度=lengthNew" + lengthNew + "||lengthOld" + lengthOld);
            Object fixedElement = Array.newInstance(componentType, totalLength);
            for (int i = 0; i < totalLength; ++i) {
                if (i < lengthNew) {
                    Object element = Array.get(newDexElements, i);
                    Array.set(fixedElement, i, element);
                } else {
                    Object element = Array.get(oldDexElements, i - lengthNew);
                    Array.set(fixedElement, i, element);
                }

            }
            return fixedElement;
        }
        return null;
    }

點選熱修復按鈕日誌這裡寫圖片描述

點選測試插入是否成功日誌
這裡寫圖片描述

總結:1.檢視安卓原始碼,理順classLoader類的關係,後面就一目瞭然,最有價值的還是第二張圖,上面都有原理和步驟
2.原始碼註釋很經典,都說的很清楚,甚至有的上面都有介紹了最新用法。