熱修復流派
基於Multidex的Dex注入
代表:TInker,手機QQ空間、Nuwa
原理:將補丁Dex對象的DexFile對象注入到系統ClassLoader相關聯的DexPathList對象的dexElements數組的最前面
Native層方法替換
代表:AndFix,阿里百川HotFix
原理:在Native層對方法的整體數據結構(Method/ArtMethod)進行替換
ClassLoader Hack
代表:Instant Run
原理:基於雙親委派機制,用自定義的IncrementalClassLoader加載補丁Dex,同時將該類加載器設置爲系統類加載器的父加載器
Tinker 熱修復Dex插樁原理
ClassLoader的加載路徑可以包含多個dex文件,每個dex文件關聯一個Element,多個dex文件排列成一個有序的數組dexElements,當查找類的時候會按順序遍歷dex文件,然後從當前遍歷的dex文件中找類,如果找到則返回,如果找不到從下一個dex文件繼續查找。理論上如果在不同的dex中有相同的類存在,那麼優先選擇排在前面的dex文件的類。
源碼分析
我們解壓一個apk,可以看到有這些.dex分包。
對應在代碼裏,BaseDexClassLoader中的pathList就是這些分包。
類加載器 ClassLoader
子類:BaseDexClassLoader
findClass是來尋找指定的類,我們可以看到,會通過pathList來尋找類。
pathList的類型是DexPathList ,我們再來看DexPathList,可以看到有一個dexElements屬性,是Dex的集合。
再來看DexPathList的構造方法
可以看到,通過makePathElements
方法,對dexElements進行賦值。
來看makePathElements
方法
private static Element[] makePathElements(List<File> files, File optimizedDirectory,
List<IOException> suppressedExceptions) {
List<Element> elements = new ArrayList<>();
//遍歷文件
for (File file : files) {
File zip = null;
File dir = new File("");
DexFile dex = null;
String path = file.getPath();
String name = file.getName();
if (path.contains(zipSeparator)) {
String split[] = path.split(zipSeparator, 2);
zip = new File(split[0]);
dir = new File(split[1]);
} else if (file.isDirectory()) {
elements.add(new Element(file, true, null, null));
} else if (file.isFile()) {
//如果文件名包含 .dex 後綴
if (name.endsWith(DEX_SUFFIX)) {
try {
//嘗試加載Dex文件
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else {
zip = file;
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException suppressed) {
suppressedExceptions.add(suppressed);
}
}
} else {
System.logW("ClassLoader referenced unknown path: " + file);
}
if ((zip != null) || (dex != null)) {
//將dex文件傳入Element中,並將element添加到elements集合中
elements.add(new Element(dir, false, zip, dex));
}
}
return elements.toArray(new Element[elements.size()]);
}
可以看到,這裏會遍歷目錄下的所有文件,然後,如果後綴匹配.dex
,則嘗試加載dex,如果加載成功,就將dex添加到elements集合中。
最終,將elements轉爲數組,並返回。
App安裝
然後我們知道,App安裝成功的時候,會複製一份apk文件到應用的私有目錄
/data/app/packageName~1/base.apk
所以可以有思路:將修復好的x.dex下載到本機,插樁到dex集合的最前面,優先加載當中修復好的java類。
實現步驟
- 自己創建一個類加載器
- 用自己創建的類加載器加載 x.dex 修復包 (得到對於的DexElements[])
- 將得到dexElements和系統的dexElements[]進行合併,並且將自有的數據放置在最前面
- 通過反射的技術,將合成後的新數組,賦值給系統的pathList
修改後的java代碼,如何生成dex ?
- 通過javac指令編譯成class文件
- 通過dx.bat轉成dex文件 (修復包)
核心步驟總結
1.獲取系統ClassLoader的pathList對象
Object pathList = Reflect.on(loader).field("pathList").get();
2.調用makePathElements構造補丁的dexElements
ArrayList<IOException> suppressedExceptions = new ArrayList<>();
Object[] patchDexElements = makePathElements(pathList,extraDexFiles,FileUtil.getDexOptDir(context),suppressedExceptions);
3.將補丁Dex注入系統ClassLoader的pathList對象的dexElements的最前面
expandElementsArray(pathList,patchDexElements);
小結
以上就是QQ空間超級補丁技術,Tinker和QQ空間超級補丁技術的原理基本相同,但對部分缺點進行了優化。
- 爲了實現修復這個過程,在應用中必須多出一個專門用作修復用的patch.dex,如果修復的類到了一定數量,就需要花不少的時間加載。
- 在ART模式下,如果類修改了結構,就會出現內存錯亂的問題。爲了解決這個問題,就必須把所有相關的調用類、父類子類等等全部加載到patch.dex中,導致補丁包異常的大,進一步增加應用啓動加載的時候,耗時更加嚴重。
Tinker針對QQ空間超級補丁技術的不足提出了一個提供DEX差量包,整體替換DEX的方案。主要的原理是與QQ空間超級補丁技術基本相同,區別在於不再將patch.dex增加到elements數組中,而是差量的方式給出patch.dex,然後將patch.dex與應用的classes.dex合併,然後整體替換掉舊的DEX文件,以達到修復的目的。
具體詳見阿里最新熱修復Sophix與QQ超級補丁和Tinker的實現與總結
類校驗異常的規避
dex插裝可能會導致該異常,所以需要破壞這3個條件的其中一個。對於Tinker,就是破壞第三個條件,就是通過全量後的方式,以避免打上預校驗標識的類,直接使類不在同一個dex中。