熱修復——深入淺出原理與實現
一、簡述 熱修復無疑是這2年較火的新技術,是作為安卓工程師必學的技能之一。在熱修復出現之前,一個已經上線的app中如果出現了bug,即使是一個非常小的bug,不及時更新的話有可能存在風險,若要及時更新就得將app重新打包釋出到應用市場後,讓使用者再一次下載,這樣就大大降低了使用者體驗,當熱修復出現之後,這樣的問題就不再是問題了。
目前較火的熱修復方案大致分為兩派,分別是:
阿里系:DeXposed、andfix:從底層二進位制入手(c語言)。 騰訊系:tinker:從java載入機制入手。 本篇的主題並非講述上面兩種方案的使用,而是基於java載入機制,來研究熱修復的原理與實現。(類似tinker,當然tinker沒這麼簡單)
二、Android中如何動態修復bug 關於bug的概念自己百度百科吧,我認為的bug一般有2種(可能不太準確):
程式碼功能不符合專案預期,即程式碼邏輯有問題。 程式程式碼不夠健壯導致App執行時崩潰。 這兩種情況一般是一個或多個class出現了問題,在一個理想的狀態下,我們只需將修復好的這些個class更新到使用者手機上的app中就可以修復這些bug了。但說著簡單,要怎麼才能動態更新這些class呢?其實,不管是哪種熱修復方案,肯定是如下幾個步驟:
下發補丁(內含修復好的class)到使用者手機,即讓app從伺服器上下載(網路傳輸) app通過“某種方式”,使補丁中的class被app呼叫(本地更新) 這裡的“某種方式”,對本篇而言,就是使用Android的類載入器,通過類載入器載入這些修復好的class,覆蓋對應有問題的class,理論上就能修復bug了。所以,下面就先來了解和分析Android中的類載入器吧。
三、Android中的類載入器 Android跟java有很大的淵源,基於jvm的java應用是通過ClassLoader來載入應用中的class的,但我們知道Android對jvm優化過,使用的是dalvik,且class檔案會被打包進一個dex檔案中,底層虛擬機器有所不同,那麼它們的類載入器當然也是會有所區別,在Android中,要載入dex檔案中的class檔案就需要用到 PathClassLoader 或 DexClassLoader 這兩個Android專用的類載入器。
1、原始碼檢視 一般的原始碼在Android Studio中可以查到,但 PathClassLoader 和 DexClassLoader 的原始碼是屬於系統級原始碼,所以無法在Android Studio中直接檢視。不過,有兩種方式可以在外部進行檢視:第一種是通過下載Android映象原始碼的方式進行檢視,但一般映象原始碼體積較大,不好下載,而且就只是為了看3、4個檔案的原始碼動不動就下載3、4個g的原始碼,確實不太明智,所以我們一般採用第二種方式:到androidxref.com這個網站上直接檢視,下面會列出之後要分析的幾個類的原始碼地址,供看客們方便瀏覽。
以下是Android 5.0中的部分原始碼:
PathClassLoader.java DexClassLoader.java BaseDexClassLoader.java DexPathList.java 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.) 1 這名註釋的意思就是說,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造成的 ,切記。
--------------------- 作者:CSDN_LQR 來源:CSDN 原文:https://blog.csdn.net/CSDN_LQR/article/details/78534065