Hotfix:讓應用能夠在無需重新安裝的情況實現更新,幫助應用快速建立動態修復能力。
熱補丁技術在2015年開始爆發,目前已經是非常熱門的Android開發技術。Android平臺出現了一些優秀的熱更新方案,主要可以分爲兩類:
- 基於multidex的熱更新框架,包括Nuwa、Tinker等;
- 基於native hook方案,如阿里開源的Andfix和Dexposed。
1. 熱更新流程
- 線上檢測到嚴重的Crash
- 拉出bugfix分支並在分支上修復問題
- jenkins構建和生成補丁
- app通過push主動拉取補丁文件
- 將bugfix合併到master上
2. Hotfix原理
ClassLoader
Java在運行時加載對應的類是通過ClassLoader來實現的,ClassLoader本身是一個抽象來,Android中使用PathClassLoader類作爲Android的默認的類加載器, PathClassLoader其實實現的就是簡單的從文件系統中加載類文件。PathClassLoade本身繼承自BaseDexClassLoader,BaseDexClassLoader重寫了findClass方法, 該方法是ClassLoader的核心。
@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;
}
BaseDexClassLoader將findClass方法委託給了pathList對象的findClass方法,pathList對象是在BaseDexClassLoader的構造函數中new出來的, 它的類型是DexPathList。
DexPathList.findClass
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列表,然後通過調用element.dexFile對象上的loadClassBinaryName方法來加載類,如果返回值不是null,就表示加載類成功,會將這個Class對象返回。
而dexElements對象是在DexPathList類的構造函數中完成初始化的。
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory, suppressedExceptions);
makeDexElements所做的事情就是遍歷我們傳遞來的dexPath,然後一次加載每個dex文件。
3. 美團Robust方案原理
Robust插件對每個產品代碼的每個函數都在編譯打包階段自動的插入了一段代碼,插入過程對業務開發是完全透明。如State.java的getIndex函數:
public long getIndex() {
return 100;
}
被處理成如下的實現:
public static ChangeQuickRedirect changeQuickRedirect;
public long getIndex() {
if(changeQuickRedirect != null) {
//PatchProxy中封裝了獲取當前className和methodName的邏輯,並在其內部最終調用了changeQuickRedirect的對應函數
if(PatchProxy.isSupport(new Object[0], this, changeQuickRedirect, false)) {
return ((Long)PatchProxy.accessDispatch(new Object[0], this, changeQuickRedirect, false)).longValue();
}
}
return 100L;
}
可以看到Robust爲每個class增加了個類型爲ChangeQuickRedirect的靜態成員,而在每個方法前都插入了使用changeQuickRedirect相關的邏輯,當 changeQuickRedirect不爲null時,可能會執行到accessDispatch從而替換掉之前老的邏輯,達到fix的目的。
如果需將getIndex函數的返回值改爲return 106,那麼對應生成的patch,主要包含兩個class:PatchesInfoImpl.java和StatePatch.java。
PatchesInfoImpl.java:
public class PatchesInfoImpl implements PatchesInfo {
public List<PatchedClassInfo> getPatchedClassesInfo() {
List<PatchedClassInfo> patchedClassesInfos = new ArrayList<PatchedClassInfo>();
PatchedClassInfo patchedClass = new PatchedClassInfo("com.meituan.sample.d", StatePatch.class.getCanonicalName());
patchedClassesInfos.add(patchedClass);
return patchedClassesInfos;
}
}
StatePatch.java:
public class StatePatch implements ChangeQuickRedirect {
@Override
public Object accessDispatch(String methodSignature, Object[] paramArrayOfObject) {
String[] signature = methodSignature.split(“:”);
if (TextUtils.equals(signature[1], “a”)) {//long getIndex() -> a
return 106;
}
return null;
}
@Override
public boolean isSupport(String methodSignature, Object[] paramArrayOfObject) {
String[] signature = methodSignature.split(":");
if (TextUtils.equals(signature[1], "a")) {//long getIndex() -> a
return true;
}
return false;
}
}
客戶端拿到含有PatchesInfoImpl.java和StatePatch.java的patch.dex後,用DexClassLoader加載patch.dex,反射拿到PatchesInfoImpl.java這個class。拿到後,創建這個class的一個對象。然後通過這個對象的getPatchedClassesInfo函數,知道需要patch的class爲com.meituan.sample.d(com.meituan.sample.State混淆後的名字),再反射得到當前運行環境中的com.meituan.sample.d class,將其中的changeQuickRedirect字段賦值爲用patch.dex中的StatePatch.java這個class new出來的對象。這就是打patch的主要過程。通過原理分析,其實Robust只是在正常的使用DexClassLoader,所以可以說這套框架是沒有兼容性問題的。
4. 微信Tinker
簡單來說,通過完全使用了新的Dex,那樣既不出現Art地址錯亂的問題,在Dalvik也無須插樁。當然考慮到補丁包的體積,不能直接將新的Dex放在裏面。但可以將新舊兩個Dex的差異放到補丁包中,這裏可以調研的方法有以下幾個:
- BsDiff;它格式無關,但對Dex效果不是特別好,而且非常不穩定。當前微信對於so與部分資源,依然使用bsdiff算法;
- DexMerge;它主要問題在於合成時內存佔用過大,一個12M的dex,峯值內存可能達到70多M;
- DexDiff;通過深入Dex格式,實現一套diff差異小,內存佔用少以及支持增刪改的算法。
微信使用了DexDiff算法。
該方案的優勢:
- Dalvik全量合成,解決了插樁帶來的性能損耗;
- Art平臺合成small dex,解決了全量合成方案佔用Rom體積大, OTA升級以及Android N的問題;
- 大部分情況下Art.info僅僅1-20K, 解決由於補丁包可能過大的問題;
事實上,DexDiff算法變的如此複雜,怎麼樣保證它的正確性呢?微信爲此做了以下三件事情:
- 隨機組成Dex校驗,覆蓋大部分case;
- 微信200個版本的隨機Diff校驗, 覆蓋日常使用情況;
- Dex文件合成產物有效性校驗,即使算法出現問題,也只是編譯不出補丁包。
每一次DexDiff算法的更新,都需要經過以上三個Test纔可以提交,這樣DexDiff的這套算法已完成了整個閉環。