熱修復——深入淺出原理與實現

一、簡述

熱修復無疑是這2年較火的新技術,是作爲安卓工程師必學的技能之一。在熱修復出現之前,一個已經上線的app中如果出現了bug,即使是一個非常小的bug,不及時更新的話有可能存在風險,若要及時更新就得將app重新打包發佈到應用市場後,讓用戶再一次下載,這樣就大大降低了用戶體驗,當熱修復出現之後,這樣的問題就不再是問題了。

目前較火的熱修復方案大致分爲兩派,分別是:

  1. 阿里系:DeXposed、andfix:從底層二進制入手(c語言)。
  2. 騰訊系:tinker:從java加載機制入手。

本篇的主題並非講述上面兩種方案的使用,而是基於java加載機制,來研究熱修復的原理與實現。(類似tinker,當然tinker沒這麼簡單)

二、Android中如何動態修復bug

關於bug的概念自己百度百科吧,我認爲的bug一般有2種(可能不太準確):

  • 代碼功能不符合項目預期,即代碼邏輯有問題。
  • 程序代碼不夠健壯導致App運行時崩潰。

這兩種情況一般是一個或多個class出現了問題,在一個理想的狀態下,我們只需將修復好的這些個class更新到用戶手機上的app中就可以修復這些bug了。但說着簡單,要怎麼才能動態更新這些class呢?其實,不管是哪種熱修復方案,肯定是如下幾個步驟:

  1. 下發補丁(內含修復好的class)到用戶手機,即讓app從服務器上下載(網絡傳輸)
  2. app通過“某種方式”,使補丁中的class被app調用(本地更新)

這裏的“某種方式”,對本篇而言,就是使用Android的類加載器,通過類加載器加載這些修復好的class,覆蓋對應有問題的class,理論上就能修復bug了。所以,下面就先來了解和分析Android中的類加載器吧。

三、Android中的類加載器

Android跟java有很大的淵源,基於jvm的java應用是通過ClassLoader來加載應用中的class的,但我們知道Android對jvm優化過,使用的是dalvik,且class文件會被打包進一個dex文件中,底層虛擬機有所不同,那麼它們的類加載器當然也是會有所區別,在Android中,要加載dex文件中的class文件就需要用到 PathClassLoaderDexClassLoader 這兩個Android專用的類加載器。

1、源碼查看

一般的源碼在Android Studio中可以查到,但 PathClassLoaderDexClassLoader 的源碼是屬於系統級源碼,所以無法在Android Studio中直接查看。不過,有兩種方式可以在外部進行查看:第一種是通過下載Android鏡像源碼的方式進行查看,但一般鏡像源碼體積較大,不好下載,而且就只是爲了看3、4個文件的源碼動不動就下載3、4個g的源碼,確實不太明智,所以我們一般採用第二種方式:到androidxref.com這個網站上直接查看,下面會列出之後要分析的幾個類的源碼地址,供看客們方便瀏覽。

以下是Android 5.0中的部分源碼:

2、PathClassLoader與DexClassLoader的區別

1)使用場景

  • PathClassLoader:只能加載已經安裝到Android系統中的apk文件(/data/app目錄),是Android默認使用的類加載器。
  • DexClassLoader:可以加載任意目錄下的dex/jar/apk/zip文件,比PathClassLoader更靈活,是實現熱修復的重點。

2)代碼差異

因爲PathClassLoader與DexClassLoader的源碼都很簡單,我就直接將它們的全部源碼複製過來了:

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

    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}

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

通過比對,可以得出2個結論:

  • PathClassLoader與DexClassLoader都繼承於BaseDexClassLoader。
  • PathClassLoader與DexClassLoader在構造函數中都調用了父類的構造函數,但DexClassLoader多傳了一個optimizedDirectory。

3、BaseDexClassLoader

通過觀察PathClassLoader與DexClassLoader的源碼我們就可以確定,真正有意義的處理邏輯肯定在BaseDexClassLoader中,所以下面着重分析BaseDexClassLoader源碼。

1)構造函數

先來看看BaseDexClassLoader的構造函數都做了什麼:

public class BaseDexClassLoader extends ClassLoader {
    ...
    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent){
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
    ...
}
  • dexPath:要加載的程序文件(一般是dex文件,也可以是jar/apk/zip文件)所在目錄。
  • optimizedDirectory:dex文件的輸出目錄(因爲在加載jar/apk/zip等壓縮格式的程序文件時會解壓出其中的dex文件,該目錄就是專門用於存放這些被解壓出來的dex文件的)。
  • libraryPath:加載程序文件時需要用到的庫路徑。
  • parent:父加載器

*tip:上面說到的”程序文件”這個概念是我自己定義的,因爲從一個完整App的角度來說,程序文件指定的就是apk包中的classes.dex文件;但從熱修復的角度來看,程序文件指的是補丁。

因爲PathClassLoader只會加載已安裝包中的dex文件,而DexClassLoader不僅僅可以加載dex文件,還可以加載jar、apk、zip文件中的dex,我們知道jar、apk、zip其實就是一些壓縮格式,要拿到壓縮包裏面的dex文件就需要解壓,所以,DexClassLoader在調用父類構造函數時會指定一個解壓的目錄。

不過,從Android 8.0開始,BaseDexClassLoader的構造函數邏輯發生了變化,optimizedDirectory過時,不再生效,詳情可查看Android 8.0的BaseDexClassLoader.java源碼

2)獲取class

類加載器肯定會提供有一個方法來供外界找到它所加載到的class,該方法就是findClass(),不過在PathClassLoader和DexClassLoader源碼中都沒有重寫父類的findClass()方法,但它們的父類BaseDexClassLoader就有重寫findClass(),所以來看看BaseDexClassLoader的findClass()方法都做了哪些操作,代碼如下:

private final DexPathList pathList;

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
    List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
    // 實質是通過pathList的對象findClass()方法來獲取class
    Class c = pathList.findClass(name, suppressedExceptions);
    if (c == null) {
        ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
        for (Throwable t : suppressedExceptions) {
            cnfe.addSuppressed(t);
        }
        throw cnfe;
    }
    return c;
}

可以看到,BaseDexClassLoader的findClass()方法實際上是通過DexPathList對象(pathList)的findClass()方法來獲取class的,而這個DexPathList對象恰好在之前的BaseDexClassLoader構造函數中就已經被創建好了。所以,下面就來看看DexPathList類中都做了什麼。

4、DexPathList

在分析一個代碼量較多的源碼之前,我們要明確要從這段源碼中要知道些什麼?這樣纔不會在“碼海”中迷失方向,我自己就定了2個小目標,分別是:

  • DexPathList的構造函數做了什麼事?
  • DexPathList的findClass()方法是怎麼獲取class的?

爲什麼是這2個目標?因爲在BaseDexClassLoader的源碼中主要就用到了DexPathList的構造函數和findClass()方法。

1)構造函數

private final Element[] dexElements;

public DexPathList(ClassLoader definingContext, String dexPath,
        String libraryPath, File optimizedDirectory) {
    ...
    this.definingContext = definingContext;
    this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);
    ...
}

這個構造函數中,保存了當前的類加載器definingContext,並調用了makeDexElements()得到Element集合。

通過對splitDexPath(dexPath)的源碼追溯,發現該方法的作用其實就是將dexPath目錄下的所有程序文件轉變成一個File集合。而且還發現,dexPath是一個用冒號(”:”)作爲分隔符把多個程序文件目錄拼接起來的字符串(如:/data/dexdir1:/data/dexdir2:…)。

那接下來無疑是分析makeDexElements()方法了,因爲這部分代碼比較長,我就貼出關鍵代碼,並以註釋的方式進行分析:

private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) {
    // 1.創建Element集合
    ArrayList<Element> elements = new ArrayList<Element>();
    // 2.遍歷所有dex文件(也可能是jar、apk或zip文件)
    for (File file : files) {
        ZipFile zip = null;
        DexFile dex = null;
        String name = file.getName();
        ...
        // 如果是dex文件
        if (name.endsWith(DEX_SUFFIX)) {
            dex = loadDexFile(file, optimizedDirectory);

        // 如果是apk、jar、zip文件(這部分在不同的Android版本中,處理方式有細微差別)
        } else {
            zip = file;
            dex = loadDexFile(file, optimizedDirectory);
        }
        ...
        // 3.將dex文件或壓縮文件包裝成Element對象,並添加到Element集合中
        if ((zip != null) || (dex != null)) {
            elements.add(new Element(file, false, zip, dex));
        }
    }
    // 4.將Element集合轉成Element數組返回
    return elements.toArray(new Element[elements.size()]);
}

在這個方法中,看到了一些眉目,總體來說,DexPathList的構造函數是將一個個的程序文件(可能是dex、apk、jar、zip)封裝成一個個Element對象,最後添加到Element集合中。

其實,Android的類加載器(不管是PathClassLoader,還是DexClassLoader),它們最後只認dex文件,而loadDexFile()是加載dex文件的核心方法,可以從jar、apk、zip中提取出dex,但這裏先不分析了,因爲第1個目標已經完成,等到後面再來分析吧。

2)findClass()

再來看DexPathList的findClass()方法:

public Class findClass(String name, List<Throwable> suppressed) {
    for (Element element : dexElements) {
        // 遍歷出一個dex文件
        DexFile dex = element.dexFile;

        if (dex != null) {
            // 在dex文件中查找類名與name相同的類
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

結合DexPathList的構造函數,其實DexPathList的findClass()方法很簡單,就只是對Element數組進行遍歷,一旦找到類名與name相同的類時,就直接返回這個class,找不到則返回null。

爲什麼是調用DexFile的loadClassBinaryName()方法來加載class?這是因爲一個Element對象對應一個dex文件,而一個dex文件則包含多個class。也就是說Element數組中存放的是一個個的dex文件,而不是class文件!!!這可以從Element這個類的源碼和dex文件的內部結構看出。

四、熱修復的實現原理

  終於進入主題了,經過對PathClassLoader、DexClassLoader、BaseDexClassLoader、DexPathList的分析,我們知道,安卓的類加載器在加載一個類時會先從自身DexPathList對象中的Element數組中獲取(Element[] dexElements)到對應的類,之後再加載。採用的是數組遍歷的方式,不過注意,遍歷出來的是一個個的dex文件。

  在for循環中,首先遍歷出來的是dex文件,然後再是從dex文件中獲取class,所以,我們只要讓修復好的class打包成一個dex文件,放於Element數組的第一個元素,這樣就能保證獲取到的class是最新修復好的class了(當然,有bug的class也是存在的,不過是放在了Element數組的最後一個元素中,所以沒有機會被拿到而已)。

五、熱修復的簡單實現

通過前面的一堆理論之後,是時候實踐一把了。

1、得到dex格式補丁

1)修復好有問題的java文件

這一步根據bug的實際情況修改代碼即可。

2)將java文件編譯成class文件

在修復bug之後,可以使用Android Studio的Rebuild Project功能將代碼進行編譯,然後從build目錄下找到對應的class文件。

將修復好的class文件複製到其他地方,例如桌面上的dex文件夾中。需要注意的是,在複製這個class文件時,需要把它所在的完整包目錄一起復制。假設上圖中修復好的class文件是SimpleHotFixBugTest.class,則到時複製出來的目錄結構是:

3)將class文件打包成dex文件

a. dx指令程序

要將class文件打包成dex文件,就需要用到dx指令,這個dx指令類似於java指令。我們知道,java的指令有javac、jar等等,之所以可以使用這類指令,是因爲我們有安裝過jdk,jdk爲我們提供了java指令,相同的,dx指令也需要有程序來提供,它就在Android SDK的build-tools目錄下各個Android版本目錄之中。

b. dx指令的使用

dx指令的使用跟java指令的使用條件一樣,有2種選擇:

  • 配置環境變量(添加到classpath),然後命令行窗口(終端)可以在任意位置使用。
  • 不配環境變量,直接在build-tools/安卓版本 目錄下使用命令行窗口(終端)使用。

第一種方式參考java環境變量配置即可,這裏我選用第二種方式。下面我們需要用到的命令是:

dx –dex –output=dex文件完整路徑 (空格) 要打包的完整class文件所在目錄,如:

dx –dex –output=C:\Users\Administrator\Desktop\dex\classes2.dex C:\Users\Administrator\Desktop\dex

具體操作看下圖:

在文件夾目錄的空白處,按住shift+鼠標右擊,可出現“在此處打開命令行窗口”。

2、加載dex格式補丁

根據原理,可以做一個簡單的工具類:

/**
 * @創建者 CSDN_LQR
 * @描述 熱修復工具(只認後綴是dex、apk、jar、zip的補丁)
 */
public class FixDexUtils {

    private static final String DEX_SUFFIX = ".dex";
    private static final String APK_SUFFIX = ".apk";
    private static final String JAR_SUFFIX = ".jar";
    private static final String ZIP_SUFFIX = ".zip";
    public static final String DEX_DIR = "odex";
    private static final String OPTIMIZE_DEX_DIR = "optimize_dex";
    private static HashSet<File> loadedDex = new HashSet<>();

    static {
        loadedDex.clear();
    }

    /**
     * 加載補丁,使用默認目錄:data/data/包名/files/odex
     *
     * @param context
     */
    public static void loadFixedDex(Context context) {
        loadFixedDex(context, null);
    }

    /**
     * 加載補丁
     *
     * @param context       上下文
     * @param patchFilesDir 補丁所在目錄
     */
    public static void loadFixedDex(Context context, File patchFilesDir) {
        if (context == null) {
            return;
        }
        // 遍歷所有的修復dex
        File fileDir = patchFilesDir != null ? patchFilesDir : new File(context.getFilesDir(), DEX_DIR);// data/data/包名/files/odex(這個可以任意位置)
        File[] listFiles = fileDir.listFiles();
        for (File file : listFiles) {
            if (file.getName().startsWith("classes") &&
                    (file.getName().endsWith(DEX_SUFFIX)
                            || file.getName().endsWith(APK_SUFFIX)
                            || file.getName().endsWith(JAR_SUFFIX)
                            || file.getName().endsWith(ZIP_SUFFIX))) {
                loadedDex.add(file);// 存入集合
            }
        }
        // dex合併之前的dex
        doDexInject(context, loadedDex);
    }

    private static void doDexInject(Context appContext, HashSet<File> loadedDex) {
        String optimizeDir = appContext.getFilesDir().getAbsolutePath() + File.separator + OPTIMIZE_DEX_DIR;// data/data/包名/files/optimize_dex(這個必須是自己程序下的目錄)
        File fopt = new File(optimizeDir);
        if (!fopt.exists()) {
            fopt.mkdirs();
        }
        try {
            // 1.加載應用程序的dex
            PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
            for (File dex : loadedDex) {
                // 2.加載指定的修復的dex文件
                DexClassLoader dexLoader = new DexClassLoader(
                        dex.getAbsolutePath(),// 修復好的dex(補丁)所在目錄
                        fopt.getAbsolutePath(),// 存放dex的解壓目錄(用於jar、zip、apk格式的補丁)
                        null,// 加載dex時需要的庫
                        pathLoader// 父類加載器
                );
                // 3.合併
                Object dexPathList = getPathList(dexLoader);
                Object pathPathList = getPathList(pathLoader);
                Object leftDexElements = getDexElements(dexPathList);
                Object rightDexElements = getDexElements(pathPathList);
                // 合併完成
                Object dexElements = combineArray(leftDexElements, rightDexElements);
                // 重寫給PathList裏面的Element[] dexElements;賦值
                Object pathList = getPathList(pathLoader);// 一定要重新獲取,不要用pathPathList,會報錯
                setField(pathList, pathList.getClass(), "dexElements", dexElements);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 反射給對象中的屬性重新賦值
     */
    private static void setField(Object obj, Class<?> cl, String field, Object value) throws NoSuchFieldException, IllegalAccessException {
        Field declaredField = cl.getDeclaredField(field);
        declaredField.setAccessible(true);
        declaredField.set(obj, value);
    }

    /**
     * 反射得到對象中的屬性值
     */
    private static Object getField(Object obj, Class<?> cl, String field) throws NoSuchFieldException, IllegalAccessException {
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        return localField.get(obj);
    }


    /**
     * 反射得到類加載器中的pathList對象
     */
    private static Object getPathList(Object baseDexClassLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
        return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }

    /**
     * 反射得到pathList中的dexElements
     */
    private static Object getDexElements(Object pathList) throws NoSuchFieldException, IllegalAccessException {
        return getField(pathList, pathList.getClass(), "dexElements");
    }

    /**
     * 數組合並
     */
    private static Object combineArray(Object arrayLhs, Object arrayRhs) {
        Class<?> componentType = arrayLhs.getClass().getComponentType();
        int i = Array.getLength(arrayLhs);// 得到左數組長度(補丁數組)
        int j = Array.getLength(arrayRhs);// 得到原dex數組長度
        int k = i + j;// 得到總數組長度(補丁數組+原dex數組)
        Object result = Array.newInstance(componentType, k);// 創建一個類型爲componentType,長度爲k的新數組
        System.arraycopy(arrayLhs, 0, result, 0, i);
        System.arraycopy(arrayRhs, 0, result, i, j);
        return result;
    }
}

代碼雖然較長,但註釋寫得很清楚,請仔細看,這裏要說兩點:

1)Class ref in pre-verified class resolved to unexpected implementation

經反饋,這個是大家遇到的最多的一個問題,這裏我把注意事項和我的解決方法寫清楚:

a.FixDexUtils

// 合併完成
Object dexElements = combineArray(leftDexElements, rightDexElements);
// 重寫給PathList裏面的Element[] dexElements;賦值
Object pathList = getPathList(pathLoader);// 一定要重新獲取,不要用pathPathList,會報錯
setField(pathList, pathList.getClass(), "dexElements", dexElements);

在合併守Element數組後,一定要再重新獲取一遍App中的原有的pathList,不要複用前面的pathPathList,絕對會報錯(Class ref in pre-verified class resolved to unexpected implementation)。

b.Instant Run

Android Studio的Instant Run功能也是用到了熱修復的原理,在重新安裝app時並不會完整安裝,只會動態修改有更新的class部分,它會影響到測試結果,在跟着本文做試驗的同學請確保Instant Run已經關閉。

c.模擬器

我在測試的過程中,使用的是Genymotion,發現Android 4.4的模擬器一直無法打上補丁,但是Android 5.0的模擬器卻可以,真機測試也沒問題,所以建議不要使用Android 5.0以下的模擬器來測試,強烈建議用真機測試!!

2)dexPath與optimizedDirectory的目錄問題

DexClassLoader dexLoader = new DexClassLoader(
        dex.getAbsolutePath(),// 修復好的dex(補丁)所在目錄
        fopt.getAbsolutePath(),// 存放dex的解壓目錄(用於jar、zip、apk格式的補丁)
        null,// 加載dex時需要的庫
        pathLoader// 父類加載器

上面的代碼是創建一個DexClassLoader對象,其中第1個和第2個參數有個細節需要注意:

  • 參數1是dexPath,指的是補丁所有目錄,可以是多個目錄(用冒號拼接),而且可以是任意目錄,比如說SD卡。
  • 參數2是optimizedDirectory,就是存放從壓縮包時解壓出來的dex文件的目錄,但不能是任意目錄,它必須是程序所屬的目錄才行,比如:data/data/包名/xxx。

如果你把optimizedDirectory指定成SD卡目錄,則會報如下錯誤:

java.lang.IllegalArgumentException: Optimized data directory /storage/emulated/0/opt_dex is not owned by the current user. Shared storage cannot protect your application from code injection attacks.

意思是說SD卡目錄不屬於當前用戶。此外,這裏再校正之前的一個小問題,optimizedDirectory不僅僅存放從壓縮包出來的dex文件,如果補丁文件就是一個dex文件,那麼它也會將這個補丁文件複製到optimizedDirectory目錄下。

3、加載jar、apk、zip格式補丁

前面已經說了很多次DexClassLoader可以加載jar、apk、zip格式補丁文件了,那這類格式的補丁文件有什麼要求嗎?
答案是:這類壓縮包中必須放着一個dex文件,而且對名字有要求,必須是classes.dex。Why?這就需要分析DexPathList類中的loadDexFile()方法了。

private static DexFile loadDexFile(File file, File optimizedDirectory)
        throws IOException {
    // 如果optimizedDirectory爲null,其實就是PathClassLoader加載dex文件的處理方式
    if (optimizedDirectory == null) {
        return new DexFile(file);
    } 
    // 如果optimizedDirectory不是null,這就是DexClassLoader加載dex文件的處理方式了,重點看這個
    else {
        String optimizedPath = optimizedPathFor(file, optimizedDirectory);
        return DexFile.loadDex(file.getPath(), optimizedPath, 0);
    }
}

參數一file,可能是dex文件,也可能是jar、apk、zip文件。

從上面的源碼中,不難看出else分支纔是DexClassLoader加載dex文件的處理方式,它調用的是optimizedPathFor()方法拿到之後dex文件在optimizedDirectory目錄下的全路徑:

private static String optimizedPathFor(File path, File optimizedDirectory) {
    String fileName = path.getName();
    if (!fileName.endsWith(DEX_SUFFIX)) {
        int lastDot = fileName.lastIndexOf(".");
        // 如果補丁沒有後綴,就給它加一個".dex"後綴
        if (lastDot < 0) {
            fileName += DEX_SUFFIX;
        } 
        // 不管補丁後綴是dex、jar、apk還是zip,最終放到optimizedDirectory目錄下的一定是dex文件
        else {
            StringBuilder sb = new StringBuilder(lastDot + 4);
            sb.append(fileName, 0, lastDot);
            sb.append(DEX_SUFFIX);
            fileName = sb.toString();
        }
    }

    File result = new File(optimizedDirectory, fileName);
    return result.getPath();
}

前面已經說過了,Android的類加載器最終只認dex文件,即使補丁是jar、apk、zip等壓縮文件,它也會把其中的dex文件解壓出來,所以該方法得到的文件名一定是以dex結尾的。好了,這個optimizedPathFor()方法並不是重點,回頭看loadDexFile()中的else分支還有一個DexFile.loadDex()方法,這個方法就相當重要了。

static public DexFile loadDex(String sourcePathName, String outputPathName, int flags) throws IOException {
    return new DexFile(sourcePathName, outputPathName, flags);
}

這個方法中就調用了一下自己的構造函數,並傳入各個參數,接着來看看DexFile的構造函數:

/**
 * Open a DEX file, specifying the file in which the optimized DEX
 * data should be written.  If the optimized form exists and appears
 * to be current, it will be used; if not, the VM will attempt to
 * regenerate it.
 *
 * This is intended for use by applications that wish to download
 * and execute DEX files outside the usual application installation
 * mechanism.  This function should not be called directly by an
 * application; instead, use a class loader such as
 * dalvik.system.DexClassLoader.
 *
 * @param sourcePathName
 *  Jar or APK file with "classes.dex".  (May expand this to include
 *  "raw DEX" in the future.)
 * @param outputPathName
 *  File that will hold the optimized form of the DEX data.
 * @param flags
 *  Enable optional features.  (Currently none defined.)
 * @return
 *  A new or previously-opened DexFile.
 * @throws IOException
 *  If unable to open the source or output file.
 */
private DexFile(String sourceName, String outputName, int flags) throws IOException {
    if (outputName != null) {
        try {
            String parent = new File(outputName).getParent();
            if (Libcore.os.getuid() != Libcore.os.stat(parent).st_uid) {
                throw new IllegalArgumentException("Optimized data directory " + parent
                        + " is not owned by the current user. Shared storage cannot protect"
                        + " your application from code injection attacks.");
            }
        } catch (ErrnoException ignored) {
            // assume we'll fail with a more contextual error later
        }
    }

    mCookie = openDexFile(sourceName, outputName, flags);
    mFileName = sourceName;
    guard.open("close");
    //System.out.println("DEX FILE cookie is " + mCookie + " sourceName=" + sourceName + " outputName=" + outputName);
}

奇怪嗎,這次我沒有把構造函數的註釋去掉,原因是在它的註釋中就已經有我們想要的答案了:

@param sourcePathName Jar or APK file with "classes.dex".  (May expand this to include "raw DEX" in the future.)

這名註釋的意思就是說,jar或apk格式的補丁文件中需要有一個classes.dex。至此,對於壓縮格式的補丁文件的要求就弄明白了。那麼接下來就只需要生成這幾種格式的補丁試一試就好了。製作這類壓縮文件也很簡單,直接用壓縮軟件壓縮成zip文件,然後改下後綴就可以。

六、測試

這部分其實本不想寫的,因爲比較簡單,但想了想不寫又覺得不完整,那接下來就來測試一波吧。

1、代碼

1)Activity

佈局文件就倆按鈕,很簡單就不貼布局文件代碼了,看這兩個按鈕的點擊事件就行。

public class SimpleHotFixActivity extends AppCompatActivity {

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_simple_hot_fix);
    }

    // “修復”按鈕的點擊事件
    public void fix(View view) {
        FixDexUtils.loadFixedDex(this, Environment.getExternalStorageDirectory());
    }

    // “計算”按鈕的點擊事件
    public void clac(View view) {
        SimpleHotFixBugTest test = new SimpleHotFixBugTest();
        test.getBug(this);
    }
}

可以看到,“修復”按鈕的點擊事件是去加載SD卡目錄下的補丁文件。

2)SimpleHotFixBugTest

public class SimpleHotFixBugTest {
    public void getBug(Context context) {
        int i = 10;
        int a = 0;
        Toast.makeText(context, "Hello,I am CSDN_LQR:" + i / a, Toast.LENGTH_SHORT).show();
    }
}

會發生什麼事呢?除數是0異常,一個簡單的運行時異常,修復它也很簡單,把a的值改爲非0即可。

2、演示

1、bug

不多說,看操作。

妥妥的ArithmeticException。

Caused by: java.lang.ArithmeticException: divide by zero

2、動態修復bug

首先,我將補丁文件classes2.dex放到手機的SD目錄下。

然後先點擊修復按鈕,再點計算按鈕。

大功告成,壓縮格式的補丁跟dex格式的補丁一樣,直接丟掉SD卡目錄下就行了,但一定要注意,壓縮格式的補丁中的文件一定是classes.dex!!!

最後貼下Demo地址

https://github.com/GitLqr/HotFixDemo

權限申請:本文的提供的Demo是讀取SD卡下的補丁文件,但卻沒有爲Android6.0以上適配動態權限申請,如果你有使用該demo進行測試,那要注意自己測試機的Android版本,若是6.0以上,請務必先爲Demo分配SD卡讀寫操作權限,否則App崩潰都不知道是不是因爲bug造成的 ,切記。

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