熱修復的原理與實現的demo

算是記錄知識的博客,所以精簡爲主。

熱修復分爲兩種:

Java層修復,以QZone,Tinker爲代表。需要重新啓動後才能完成修復。

Native層修復,以阿里係爲代表。可以達成及時修復。

實現demo用的是Java層的修復。java層修復的原理是通過類加載器的機制來實現——雙親委派機制。

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

 這上面是系統源碼,可以看出在加載一個class的時候,他會先讓父類去加載,父類如果可以加載,那就父類加載,父類加載不了,那就會拋出一個異常,讓子類去加載。這麼做的主要原因就是可以避免一個類被重複加載。所以你肯定遇到過使用第三方庫,編譯時提示你class重複這個問題。那麼類替換的熱修復方案就是,讓fix類先被加載,這樣有問題的class就不會被加載了

在5.0之前會有一個CLASS_ISPREVERIFIED的錯誤。這個錯誤的解釋是:

  1. 在apk安裝的時候,虛擬機會將dex優化成odex後纔拿去執行。在這個過程中會對所有class一個校驗。
  2. 校驗方式:假設A該類在它的static方法,private方法,構造函數,override方法中直接引用到B類。如果A類和B類在同一個dex中,那麼A類就會被打上CLASS_ISPREVERIFIED標記
  3. 被打上這個標記的類不能引用其他dex中的類,否則就會報圖中的錯誤
  4. 在我們的Demo中,MainActivity和Cat本身是在同一個dex中的,所以MainActivity被打上了CLASS_ISPREVERIFIED。而我們修復bug的時候卻引用了另外一個dex的Cat.class,所以這裏就報錯了
  5. 而普通分包方案則不會出現這個錯誤,因爲引用和被引用的兩個類一開始就不在同一個dex中,所以校驗的時候並不會被打上CLASS_ISPREVERIFIED

上文是在其他博客中引用的。如果你的minSDKVersion大於5.0就不用考慮這件事兒了。上面解決方案是插樁,這裏不做贅述。

首先需要把一個class變成dex,使用SDK下Tools自帶的dx工具就行了。

dx --dex --output out.dex in.class

記住class需要放在相應的包名路徑中。

得到了class之後,就用下面的工具類就行。


/**
 * 修復的工具類
 * on 2020/4/6
 */
public class FixUtil {

    /**
     * 講dex存到自己的文件夾下
     * @param context
     * @param dirName 存放dex的目錄
     * @param dexName dex名字
     */
    public static void loadDex(Context context,String dirName,String dexName) {
        //構建存放dex目錄
        File dexDir = context.getDir(dirName, Context.MODE_PRIVATE);
        //拿到dex文件拷貝
        String dexPath = dexDir.getAbsoluteFile() + File.separator + dexName;
        File dexFile = new File(dexPath);
        if (!dexFile.exists()) {
            try {
                dexFile.createNewFile();
                FixUtil.copyFiles(context, dexName, dexFile);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
    
    
    
    public static void FixDex(Context context,File dexDir){
        //1.獲得要修復的dexList
        List<File> dexList = getDexList(context, dexDir);

        try {
            //2.找到pathList對象
            Field pathListField = FixUtil.getField(context.getClassLoader(), "pathList");
            Object pathList = pathListField.get(context.getClassLoader());
            //3.獲取系統的dexElements
            Field dexElementsField = FixUtil.getField(pathList, "dexElements");
            Object[] oldDexElements = (Object[]) dexElementsField.get(pathList);
            //4.獲取makeDexElements
            ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
            Method makeDexElements = FixUtil.getMethod(pathList, "makeDexElements", List.class, File.class
                    , List.class, ClassLoader.class);
            //5.加載修復的dexList
            Object[] fixDexElements = (Object[]) makeDexElements.invoke(pathList, dexList, dexDir, suppressedExceptions,
                    context.getClassLoader());
            //6.創建新的Elements數組
            Object[] newDexElements = (Object[]) Array.newInstance(oldDexElements.getClass().getComponentType()
                    , oldDexElements.length + fixDexElements.length);
            //7.合併Elements數組
            System.arraycopy(fixDexElements, 0, newDexElements, 0, fixDexElements.length);
            System.arraycopy(oldDexElements, 0, newDexElements, fixDexElements.length, fixDexElements.length);
            //8.將系統的Elements重新設置
            dexElementsField.set(pathList, newDexElements);
            Log.d("TAG", "修復完成");
        } catch (Exception e) {
            Log.d("TAG", "修復失敗" + e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 獲取文件夾下所有.dex文件
     * @param context
     * @param dexDir
     * @return
     */
    public static List<File> getDexList(Context context, File dexDir) {
        File[] files = dexDir.listFiles();
        List<File> fileList = new ArrayList<>();
        for (File file : files) {
            if (file.getName().endsWith(".dex")) {
                fileList.add(file);
            }
        }

        return fileList;
    }


    /**
     * 將文件拷貝到私有目錄下
     * @param context
     * @param fileName
     * @param desFile
     */
    public static void copyFiles(Context context, String fileName, File desFile) {
        InputStream in = null;
        OutputStream out = null;

        try {
            in = context.getAssets().open(fileName);
            out = new FileOutputStream(desFile.getAbsolutePath());
            byte[] bytes = new byte[1024];
            int len = 0;
            while ((len = in.read(bytes)) != -1)
                out.write(bytes, 0, len);
            out.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (in != null)
                    in.close();
                if (out != null)
                    out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }


    /**
     * 反射屬性
     * @param object
     * @param name
     * @return
     * @throws NoSuchFieldException
     */
    public static Field getField(Object object, String name) throws NoSuchFieldException {
        Class<?> aClass = object.getClass();
        while (aClass != null) {
            try {
                Field declaredField = aClass.getDeclaredField(name);
                if (!declaredField.isAccessible()) {
                    declaredField.setAccessible(true);
                }
                return declaredField;
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
                aClass = aClass.getSuperclass();
            }

        }
        throw new NoSuchFieldException("找不到對應屬性");
    }

    /**
     * 反射方法
     * @param object
     * @param name
     * @param param
     * @return
     * @throws NoSuchMethodException
     */
    public static Method getMethod(Object object, String name, Class... param) throws NoSuchMethodException {
        Class<?> aClass = object.getClass();
        while (aClass != null) {
            try {
                Method declaredField = aClass.getDeclaredMethod(name, param);
                if (!declaredField.isAccessible()) {
                    declaredField.setAccessible(true);
                }
                return declaredField;
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
                aClass = aClass.getSuperclass();
            }

        }
        throw new NoSuchMethodException("找不到對應屬性");
    }


}

我將dex存放在Asset文件夾下模擬的網絡加載,如果是網絡加載的話替換下這塊就行。然後記住在Application裏面去加載你存放dex文件夾下面的所有fix文件。爲什麼要重新啓動才能加載?因爲你沒法去把一個已經加載的class給卸載咯。還有記住別開混淆!

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章