國際慣例先貼地址 Tinker開源地址:https://github.com/Tencent/tinker
玩過Dota的童鞋都知道 地精修補匠的大招,我們希望發版本可以像它一樣做到無限刷新。 Android熱補丁技術應該分爲以下兩個流派:
- Native,代表有阿里的Dexposed、AndFix與騰訊的內部方案KKFix;
- Java,代表有Qzone的超級補丁、大衆點評的nuwa、百度金融的rocooFix, 餓了麼的amigo以及美團的robust。 Native流派與Java流派都有着自己的優缺點。事實上從來都沒有最好的方案,只有最適合自己的。
Native的代表Dexposed/AndFix;最大挑戰在於穩定性與兼容性,而且native異常排查難度更高。另一方面,由於無法增加變量與類等限制,無法做到功能發佈級別; java的代表Qzone;最大挑戰在於性能,即Dalvik平臺存在插樁導致的性能損耗,Art平臺由於地址偏移問題導致補丁包可能過大的問題;
微信針對QQ空間超級補丁技術的不足提出了一個提供DEX差量包,整體替換DEX的方案。主要的原理是與QQ空間超級補丁技術基本相同,區別在於不 再將patch.dex增加到elements數組中,而是差量的方式給出patch.dex,然後將patch.dex與應用的classes.dex 合併,然後整體替換掉舊的DEX,達到修復的目的。
這裏有個問題很關鍵,Tinker的亮點使用了QQ空間插樁的效果來規避Android的校驗機制。NUWA分析裏面有具體介紹。簡單來說dvm有一條規則: 一個類如果引用了另一個類,一般是要求他們由同一個dex加載.上面的流程顯然犯規了,補丁肯定不和原來的類是同一個dex.但爲什麼MultiDex這 類分包方案不犯規呢?是因爲判斷犯規有個條件,即如果類沒有被打上IS_PREVERIFIED標記則不會觸發判定.如果類在靜態代碼塊或構造函數中引用 到了不在同一個dex的文件則不會有IS_PREVERIFIED標記.因此最直接的辦法就是手動在所有類的構造函數或static函數中加上一行引用其 他dex的方法,這個dex出於性能考慮只有一個空的類比如class A {}.這個dex叫做hack dex, 給所有類加引用的步驟叫做"插樁".這也是目前nuwa目前所使用的手段,當然了,手動插樁是不現實的,一般會用JavaAssist做字節碼層面的修 改,但好像用AspectJ也可以~好處是源碼級的改動,不需要做字節碼的操作,但目前沒人這麼搞過 首先看下源碼,最新源碼是dev分支tags 1.6.2 https://github.com/Tencent/tinker/tree/dev/tinker-android/tinker-android-loader/src/main/java/com/tencent/tinker/loader
2016-10-08 09:51:30屏幕截圖.png
從類名可以知道Tinker處理了類的加載,資源的加載以及so庫的加載.我們的關注點在類加載上,根據經驗判斷,TinkerLoader類是類加載模塊的入口,因此從該類開始:
@Override
public Intent tryLoad(TinkerApplication app, int tinkerFlag, boolean tinkerLoadVerifyFlag) {
Intent resultIntent = new Intent();
long begin = SystemClock.elapsedRealtime();
tryLoadPatchFilesInternal(app, tinkerFlag, tinkerLoadVerifyFlag, resultIntent);
long cost = SystemClock.elapsedRealtime() - begin;
ShareIntentUtil.setIntentPatchCostTime(resultIntent, cost);
return resultIntent;
}
TinkerLoader.tryLoad()很明顯就是加載dex的入口函數,這裏微信統計了加載時間,並進入tryLoadPatchFilesInternal()方法.這個方法較長,主要是對新舊兩個dex做合併,這裏截取其中關鍵的步驟:
if (isEnabledForDex) {
//tinker/patch.info/patch-641e634c/dex
boolean dexCheck = TinkerDexLoader.checkComplete(patchVersionDirectory, securityCheck, resultIntent);
if (!dexCheck) {
//file not found, do not load patch
Log.w(TAG, "tryLoadPatchFiles:dex check fail");
return;
}
}
做了很多安全校驗的機制以保證dex可用後,調用TinkerDexLoader.loadTinkerJars()方法. loadTinkerJars()獲取PathClassLoader並讀取dex與dvm優化後的odex地址,
具體代碼請查看原文(http://www.jianshu.com/p/11acde51ff0b) 或請點擊下方查看原文
接着遍歷dexList,過濾md5不符校驗不通過的,調用SystemClassLoaderAdder的 installDexs()方法.
public static void installDexes(Application application, PathClassLoader loader, File dexOptDir, List<File> files)throws Throwable { if (!files.isEmpty()) { ClassLoader classLoader = loader;if (Build.VERSION.SDK_INT >= 24) { classLoader = AndroidNClassLoader.inject(loader, application); }//because in dalvik, if inner class is not the same classloader with it wrapper class.//it won't fail at dex2optif (Build.VERSION.SDK_INT >= 23) { V23.install(classLoader, files, dexOptDir); } else if (Build.VERSION.SDK_INT >= 19) { V19.install(classLoader, files, dexOptDir); } else if (Build.VERSION.SDK_INT >= 14) { V14.install(classLoader, files, dexOptDir); } else { V4.install(classLoader, files, dexOptDir); }if (!checkDexInstall()) {throw new TinkerRuntimeException( ShareConstants.CHECK_DEX_INSTALL_FAIL); } } }
可以看到Tinker對不同系統版本分開做了處理,這裏我們就看使用最廣泛的Android4.4到Android5.1.
/** * Installer for platform versions 19. */private static final class V19 {private static void install( ClassLoader loader, List<File> additionalClassPathEntries, File optimizedDirectory) throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException, InvocationTargetException, NoSuchMethodException, IOException { /* The patched class loader is expected to be a descendant of * dalvik.system.BaseDexClassLoader. We modify its * dalvik.system.DexPathList pathList field to append additional DEX * file entries. */Field pathListField = ShareReflectUtil.findField(loader, "pathList"); Object dexPathList = pathListField.get(loader); ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>(); ShareReflectUtil.expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList, new ArrayList<File>(additionalClassPathEntries), optimizedDirectory, suppressedExceptions));if (suppressedExceptions.size() > 0) { for (IOException e : suppressedExceptions) { Log.w(TAG, "Exception in makeDexElement", e);throw e; } } }
V19.install()中先通過反射獲取BaseDexClassLoader中的dexPathList,然後調用了 ShareReflectUtil.expandFieldArray().值得一提的是微信對異常的處理很細緻,用List接收dexElements 數組中每一個dex加載拋出的異常而不是籠統的拋出一個大異常.
接着跟到shareutil包下的ShareReflectUtil類,不要被它的註釋誤導了,這裏不是替換普通的Field,調用這個方法的入參fieldName正是上一步中的”dexElements”,在這麼不起眼的一個工具類中終於找到了Dex流派的核心方法。
/** public static void expandFieldArray(Object instance, String fieldName, Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { Field jlrField = findField(instance, fieldName); //這句是關鍵,這裏的jlrField也就是所謂的dexElements Object[] original = (Object[]) jlrField.get(instance); Object[] combined = (Object[]) Array.newInstance( original.getClass().getComponentType(), original.length + extraElements.length); // NOTE: changed to copy extraElements first, for patch load first System.arraycopy(extraElements, 0, combined, 0, extraElements.length); System.arraycopy(original, 0, combined, extraElements.length, original.length); jlrField.set(instance, combined); }
Tinker本質仍然是用dexElements中位置靠前的Dex優先加載類來實現熱修復: )(ps:並沒有傳說那麼先進)
Tinker雖然原理不變,但它也有拿得出手的重大優化:傳統的插樁步驟會導致第一次加載類時耗時變長.應用啓動時通常會加載大量類,所以對啓動時 間的影響很可觀.Tinker的亮點是通過全量替換dex的方式避免unexpectedDEX,這樣做所有的類自然都在同一個dex中.但這會帶來補丁 包dex過大的問題,由此微信自研了DexDiff算法來取代傳統的BsDiff,極大降低了補丁包大小,又規避了運行性能問題又減小了補丁包大小,可以 說是Dex流派的一大進步.
簡單來說,在編譯時通過新舊兩個Dex生成差異path.dex。在運行時,將差異patch.dex重新跟原始安裝包的舊Dex還原爲新的 Dex。這個過程可能比較耗費時間與內存,所以我們是單獨放在一個後臺進程:patch中。爲了補丁包儘量的小,微信自研了DexDiff算法,它深度利 用Dex的格式來減少差異的大小。它的粒度是Dex格式的每一項,可以充分利用原本Dex的信息,而BsDiff的粒度是文件,AndFix/QZone 的粒度爲class。
關於微信所使用的三種算法,如圖所示
BsDiff;它格式無關,但對Dex效果不是特別好,而且非常不穩定。當前微信對於so與部分資源,依然使用bsdiff算法;
DexMerge;它主要問題在於合成時內存佔用過大,一個12M的dex,峯值內存可能達到70多M;
DexDiff;通過深入Dex格式,實現一套diff差異小,內存佔用少以及支持增刪改的算法。
由於微信發佈的Android_N混合編譯與對熱補丁影響解析,所以在tinker中完全使用了新的Dex,那樣既不出現Art地址錯亂的問題,在Dalvik也無須插樁。當然考慮到補丁包的體積,我們不能直接將新的Dex放在裏面。但我們可以將新舊兩個Dex的差異放到補丁包中
關於算法這塊不再做過多介紹,根據騰訊bugly說後面會出文章詳細說明。
整體的流程如下:
從流程圖來看,同樣可以很明顯的找到這種方式的特點:
優勢: 合成整包,不用在構造函數插入代碼,防止verify,verify和opt在編譯期間就已經完成,不會在運行期間進行 性能提高。兼容性和穩定性比較高。 開發者透明,不需要對包進行額外處理。 不足: 與超級補丁技術一樣,不支持即時生效,必須通過重啓應用的方式才能生效。 需要給應用開啓新的進程才能進行合併,並且很容易因爲內存消耗等原因合併失敗。 合併時佔用額外磁盤空間,對於多DEX的應用來說,如果修改了多個DEX文件,就需要下發多個patch.dex與對應的classes.dex進行合併操作時這種情況會更嚴重,因此合併過程的失敗率也會更高。
目前熱補丁各式各樣,眼花繚亂啊。。。。思密達。。請勿轉載使用,~~~~