Tinker Android熱補丁框架

國際慣例先貼地址 Tinker開源地址:https://github.com/Tencent/tinkerjava

玩過Dota的童鞋都知道 地精修補匠的大招,咱們但願發版本能夠像它同樣作到無限刷新。
Android熱補丁技術應該分爲如下兩個流派:android

  • Native,表明有阿里的Dexposed、AndFix與騰訊的內部方案KKFix;git

  • Java,表明有Qzone的超級補丁、大衆點評的nuwa、百度金融的rocooFix, 餓了麼的amigo以及美團的robust。
    Native流派與Java流派都有着本身的優缺點。事實上歷來都沒有最好的方案,只有最適合本身的。github

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類是類加載模塊的入口,所以從該類開始:app

@Overrideide


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進行合併操做時這種狀況會更嚴重,所以合併過程的失敗率也會更高。

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


本文分享自微信公衆號 - 喘口仙氣(gh_db8538619cdd)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索