熱補丁方案有很多,其中比較出名的有騰訊Tinker、阿里的AndFix、美團的Robust以及QZone的超級補丁方案。他們的優劣如下:
一、Tinker 熱修復
Tinker通過 Dexdiff 算法將原apk和修復後的apk中的dex文件進行對比,生成差分包,運行時將差分包中的dex和原包中的dex進行合併,從而加載差分包中修復好的類。因爲是運行時加載的dex文件,所以修復完成後不能即時生效,需要重啓app。
二、Qzone熱修復
QQ空間的熱修復原理和tinker有異曲同工之處,它基於dex分包方案,把bug類修復完成之後,單獨生成一個dex文件,運行期間加載dex補丁,運行的是修復後的類。在Android中所有我們運行期間需要的類都是由ClassLoader(類加載器)進行加載,因此讓ClassLoader加載全新的類替換掉出現Bug的類即可完成熱修復。所以也需要重啓才能生效。
三、AndFix熱修復
在native動態替換java層的方法,通過native層hook java層的代碼。執行方法時,會直接將修復後的方法再native層進行替換,達到修復的效果,這種方式修復後直接會生效,不需要重啓。
四、Robust美團熱修復方案
方法運行時會在方法內插入一段代碼,如果有修復內容,會將執行的代碼重定向到其他方法中。
參考了Instans Run的原理。這種方案也是不需要重啓的
五、我們基於QQ空間的熱修復方案進行研究
1. ART與Dalvik
什麼是Dalvik:
Dalvik是Google公司自己設計用於Android平臺的Java虛擬機。支持已轉換爲.dex(Dalvik Executable)格式的Java應用程序的運行,.dex格式是專爲Dalvik應用設計的一種壓縮格式,適合內存和處理器速度有限的系統。
什麼是ART:
Android Runtime, Android 4.4 中引入的一個開發者選項,也是 Android 5.0 及更高版本的默認模式。在應用安裝的時候Ahead-Of-Time(AOT)預編譯字節碼到機器語言,這一機制叫Ahead-Of-Time(AOT)預編譯。應用程序安裝會變慢,但是執行將更有效率,啓動更快。
在Dalvik下,應用運行需要解釋執行,常用熱點代碼通過即時編譯器(JIT)將字節碼轉換爲機器碼,運行效率低。而在ART 環境中,應用在安裝時,字節碼預編譯(AOT)成機器碼,安裝慢了,但運行效率會提高。
ART佔用空間比Dalvik大(字節碼變爲機器碼), “空間換時間"。
預編譯也可以明顯改善電池續航,因爲應用程序每次運行時不用重複編譯了,從而減少了 CPU 的使用頻率,降低了能耗。
Dexopt與DexAot
這兩個操作是Art架構安裝時的操作, ART會執行AOT,但針對Dalvik 開發的應用也能在 ART 環境中運作。
dexopt:對dex文件進行驗證和優化,優化後的格式爲odex(Optimized dex) 文件
dexAot:在安裝時對 dex 文件執行dexopt優化之後,再將odex進行 AOT 提前編譯操作,編譯爲OAT可執行文件(機器碼)
2. ClassLoader
Java 類加載器
BootClassLoader , 用於加載Android Framework層class文件。
PathClassLoader ,用於Android應用程序類加載器。可以加載指定的dex,以及jar、zip、apk中的classes.dex
DexClassLoader,加載指定的,以及jar、zip、apk 中的classes.dex。
我們可以在activity中打印來進行驗證:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/*
* 測試classLoader的一些使用情況
*/
// 我們外部的類都是用的PathClassLoader
ClassLoader classLoader1 = this.getClassLoader();
LogUtils.i("loader1 === " + classLoader1);
// 父加載器就是BootClassLoader,所以這個類先從framework中去查找,找不到就從我們本地中查找
LogUtils.i("loader1 parent === " + classLoader1.getParent());
// framework層的類加載都是用的BootClassLoader
ClassLoader classLoader2 = Activity.class.getClassLoader();
LogUtils.i("loader === " + classLoader2);
}
打印結果:
3. 源碼跟蹤
在虛擬機中,加載一個類時,使用的時ClassLoader中的loadClass方法進行加載的,看一下源碼:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
// 一個類加載後會加入到緩存中,以後加載時從緩存中讀取就可以了
// 如果找不到就從父親classLoader中查找
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;
}
代碼中可以看到,虛擬機加載一個類時,會從父ClassLoader中查找這個類,父ClassLoade找不到會遞歸到父親的父親,如果祖輩都找不到時,纔會使用當前的ClassLoader進行查找。這就是傳說中的雙親委託機制。爲什麼這樣做呢?
1、避免重複加載,當父加載器已經加載了該類的時候,就沒有必要子ClassLoader再加載一次。
2、安全性考慮,防止核心API庫被隨意篡改。
顯而易見,這樣不管是父加載器還是自己,都會走到findClass()方法:
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
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;
}
ClassLoader中findCliss方法只拋出了一個異常?那肯定是它的子類重寫並實現它了~, 在android中,ClassLoader都是繼承了BaseDexClassLoader(可以看PathClassLoader和DexClassLoader, 往上有些人說PathClassLoader可以加載內部類,DexClassLoader纔可以加載外部存儲卡的文件,其實這兩者都可以加載, 沒有任何區別),以上代碼就是在BaseDexClassLoader中實現了類的查找。裏面是通過pathList來進行查找的。繼續看pathList(DexPathList.java)中的實現:
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
可以看到,dexElements是一個數組, 這裏遍歷了dexElements,DexFile對象可以看作是dex文件, 如果找到了類直接返回了,這裏驗證了我們上面所說的。QQ空間熱修復就是將修復包patch.dex加入到dexElements開始的位置,當虛擬機加載類時,會先從patch.dex中查找,找到了直接返回,找不到還使用原來的,這樣就達到了熱修復的效果。
dexElements是一個Element類型的數組,源碼中這個Element是私有的,如何創建新的Element並加入到dexElements中呢? 先來看看源碼中的dexElements是怎麼創建的:
// save dexPath for BaseDexClassLoader
this.dexElements = makePathElements(splitDexPath(dexPath), optimizedDirectory,
suppressedExceptions);
在pathList的構造方法中可以看到,通過makePathElements可以創建一個element數組,所以我們通過反射來調用makePathElements方法創建一個新的數組,再獲取到原數組,將兩個數組合併到dexElements就可以了。
拿SDK23舉例:
首先通過classloader找到pathList對象,
再執行pathList中的makePathElements方法創建補丁包的Element數組
反射拿到原來的dexElements數組
將兩個數組進行合併,放到一個新的數組中
再反射修改dexElements,將新數組覆蓋調原來的數組,完成熱修復。
代碼如下:
public static void install(ClassLoader classLoader,
File patch) {
List<File> patchs = new ArrayList<>();
patchs.add(patch);
// 查找pathList字段
Field pathListField = ReflectUtils.findField(classLoader, "pathList");
// 1. 獲取pathList對象
try {
Object pathList = pathListField.get(classLoader);
if (pathList == null) {
throw new RuntimeException("pathList對象爲空");
}
Method method = ReflectUtils.findMethod(pathList, "makePathElements", List.class, File.class, List.class);
ArrayList<IOException> suppressedExceptions = new ArrayList<>();
// 2. 補丁包的elements數組
Object[] patchElements = (Object[]) method.invoke(null, patchs, null, suppressedExceptions);
Field dexElementsField = ReflectUtils.findField(pathList, "dexElements");
// 3. 原來的dex數組
Object[] oldElements = (Object[]) dexElementsField.get(pathList);
// 進行合併
// 4. 首先利用反射創建一個盛放兩個數組的新數組
Object[] newElements = (Object[]) Array.newInstance(oldElements.getClass().getComponentType(),
oldElements.length + patchElements.length);
// 5. 將兩個數組放到新數組中,補丁包的要放在前面
System.arraycopy(patchElements, 0, newElements, 0, patchElements.length);
System.arraycopy(oldElements, 0, newElements, patchElements.length, oldElements.length);
// 6. 將原來的dexElement數組用新數組替換掉
dexElementsField.set(pathList, newElements);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
LogUtils.i("error == " + e.getTargetException().getMessage());
}
}
sdk23 , 19 , 14, 4 這些版本創建dexElement數組的方式不一樣,或許是方法名不同,或許是參數不同,需要對這幾個版本單獨做適配,這裏只列舉了sdk23的反射方法,其他版本原理相同。同時,這一部分內容可參考Tinker熱修復方案來進行適配:tinker方案