Tinker Android熱補丁框架

國際慣例先貼地址 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進行合併操作時這種情況會更嚴重,因此合併過程的失敗率也會更高。

目前熱補丁各式各樣,眼花繚亂啊。。。。思密達。。請勿轉載使用,~~~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章