溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

如何進(jìn)行Tinker Android熱補(bǔ)丁框架的分析

發(fā)布時(shí)間:2021-12-09 10:06:39 來(lái)源:億速云 閱讀:141 作者:柒染 欄目:大數(shù)據(jù)

這篇文章將為大家詳細(xì)講解有關(guān)如何進(jìn)行Tinker Android熱補(bǔ)丁框架的分析,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個(gè)參考,希望大家閱讀完這篇文章后對(duì)相關(guān)知識(shí)有一定的了解。

Android熱補(bǔ)丁技術(shù)應(yīng)該分為以下兩個(gè)流派:

  • Native,代表有阿里的Dexposed、AndFix與騰訊的內(nèi)部方案KKFix;

  • Java,代表有Qzone的超級(jí)補(bǔ)丁、大眾點(diǎn)評(píng)的nuwa、百度金融的rocooFix, 餓了么的amigo以及美團(tuán)的robust。
    Native流派與Java流派都有著自己的優(yōu)缺點(diǎn)。事實(shí)上從來(lái)都沒(méi)有最好的方案,只有最適合自己的。

Native的代表Dexposed/AndFix;最大挑戰(zhàn)在于穩(wěn)定性與兼容性,而且native異常排查難度更高。另一方面,由于無(wú)法增加變量與類等限制,無(wú)法做到功能發(fā)布級(jí)別;
java的代表Qzone;最大挑戰(zhàn)在于性能,即Dalvik平臺(tái)存在插樁導(dǎo)致的性能損耗,Art平臺(tái)由于地址偏移問(wèn)題導(dǎo)致補(bǔ)丁包可能過(guò)大的問(wèn)題;

微信針對(duì)QQ空間超級(jí)補(bǔ)丁技術(shù)的不足提出了一個(gè)提供DEX差量包,整體替換DEX的方案。主要的原理是與QQ空間超級(jí)補(bǔ)丁技術(shù)基本相同,區(qū)別在于不 再將patch.dex增加到elements數(shù)組中,而是差量的方式給出patch.dex,然后將patch.dex與應(yīng)用的classes.dex 合并,然后整體替換掉舊的DEX,達(dá)到修復(fù)的目的。


        這里有個(gè)問(wèn)題很關(guān)鍵,Tinker的亮點(diǎn)使用了QQ空間插樁的效果來(lái)規(guī)避Android的校驗(yàn)機(jī)制。NUWA分析里面有具體介紹。簡(jiǎn)單來(lái)說(shuō)dvm有一條規(guī)則: 一個(gè)類如果引用了另一個(gè)類,一般是要求他們由同一個(gè)dex加載.上面的流程顯然犯規(guī)了,補(bǔ)丁肯定不和原來(lái)的類是同一個(gè)dex.但為什么MultiDex這 類分包方案不犯規(guī)呢?是因?yàn)榕袛喾敢?guī)有個(gè)條件,即如果類沒(méi)有被打上IS_PREVERIFIED標(biāo)記則不會(huì)觸發(fā)判定.如果類在靜態(tài)代碼塊或構(gòu)造函數(shù)中引用 到了不在同一個(gè)dex的文件則不會(huì)有IS_PREVERIFIED標(biāo)記.因此最直接的辦法就是手動(dòng)在所有類的構(gòu)造函數(shù)或static函數(shù)中加上一行引用其 他dex的方法,這個(gè)dex出于性能考慮只有一個(gè)空的類比如class A {}.這個(gè)dex叫做hack dex, 給所有類加引用的步驟叫做"插樁".這也是目前nuwa目前所使用的手段,當(dāng)然了,手動(dòng)插樁是不現(xiàn)實(shí)的,一般會(huì)用JavaAssist做字節(jié)碼層面的修 改,但好像用AspectJ也可以~好處是源碼級(jí)的改動(dòng),不需要做字節(jié)碼的操作,但目前沒(méi)人這么搞過(guò)
首先看下源碼,最新源碼是dev分支tags 1.6.2
https://github.com/Tencent/tinker/tree/dev/tinker-android/tinker-android-loader/src/main/java/com/tencent/tinker/loader

從類名可以知道Tinker處理了類的加載,資源的加載以及so庫(kù)的加載.我們的關(guān)注點(diǎn)在類加載上,根據(jù)經(jīng)驗(yàn)判斷,TinkerLoader類是類加載模塊的入口,因此從該類開(kāi)始:

@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的入口函數(shù),這里微信統(tǒng)計(jì)了加載時(shí)間,并進(jìn)入tryLoadPatchFilesInternal()方法.這個(gè)方法較長(zhǎng),主要是對(duì)新舊兩個(gè)dex做合并,這里截取其中關(guān)鍵的步驟:

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;

}

}

做了很多安全校驗(yàn)的機(jī)制以保證dex可用后,調(diào)用TinkerDexLoader.loadTinkerJars()方法.
loadTinkerJars()獲取PathClassLoader并讀取dex與dvm優(yōu)化后的odex地址,

具體代碼請(qǐng)查看原文(http://www.jianshu.com/p/11acde51ff0b)
或請(qǐng)點(diǎn)擊下方查看原文

接著遍歷dexList,過(guò)濾md5不符校驗(yàn)不通過(guò)的,調(diào)用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對(duì)不同系統(tǒng)版本分開(kāi)做了處理,這里我們就看使用最廣泛的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()中先通過(guò)反射獲取BaseDexClassLoader中的dexPathList,然后調(diào)用了 ShareReflectUtil.expandFieldArray().值得一提的是微信對(duì)異常的處理很細(xì)致,用List接收dexElements 數(shù)組中每一個(gè)dex加載拋出的異常而不是籠統(tǒng)的拋出一個(gè)大異常.

接著跟到shareutil包下的ShareReflectUtil類,不要被它的注釋誤導(dǎo)了,這里不是替換普通的Field,調(diào)用這個(gè)方法的入?yún)ieldName正是上一步中的”dexElements”,在這么不起眼的一個(gè)工具類中終于找到了Dex流派的核心方法。

/**
public static void expandFieldArray(Object instance, String fieldName, 
Object[] extraElements) throws NoSuchFieldException, IllegalArgumentException,
IllegalAccessException { Field jlrField = findField(instance, fieldName);
//這句是關(guān)鍵,這里的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本質(zhì)仍然是用dexElements中位置靠前的Dex優(yōu)先加載類來(lái)實(shí)現(xiàn)熱修復(fù): )(ps:并沒(méi)有傳說(shuō)那么先進(jìn))

Tinker雖然原理不變,但它也有拿得出手的重大優(yōu)化:傳統(tǒng)的插樁步驟會(huì)導(dǎo)致第一次加載類時(shí)耗時(shí)變長(zhǎng).應(yīng)用啟動(dòng)時(shí)通常會(huì)加載大量類,所以對(duì)啟動(dòng)時(shí) 間的影響很可觀.Tinker的亮點(diǎn)是通過(guò)全量替換dex的方式避免unexpectedDEX,這樣做所有的類自然都在同一個(gè)dex中.但這會(huì)帶來(lái)補(bǔ)丁 包dex過(guò)大的問(wèn)題,由此微信自研了DexDiff算法來(lái)取代傳統(tǒng)的BsDiff,極大降低了補(bǔ)丁包大小,又規(guī)避了運(yùn)行性能問(wèn)題又減小了補(bǔ)丁包大小,可以 說(shuō)是Dex流派的一大進(jìn)步.

簡(jiǎn)單來(lái)說(shuō),在編譯時(shí)通過(guò)新舊兩個(gè)Dex生成差異path.dex。在運(yùn)行時(shí),將差異patch.dex重新跟原始安裝包的舊Dex還原為新的 Dex。這個(gè)過(guò)程可能比較耗費(fèi)時(shí)間與內(nèi)存,所以我們是單獨(dú)放在一個(gè)后臺(tái)進(jìn)程:patch中。為了補(bǔ)丁包盡量的小,微信自研了DexDiff算法,它深度利 用Dex的格式來(lái)減少差異的大小。它的粒度是Dex格式的每一項(xiàng),可以充分利用原本Dex的信息,而B(niǎo)sDiff的粒度是文件,AndFix/QZone 的粒度為class。

關(guān)于微信所使用的三種算法,如圖所示

如何進(jìn)行Tinker Android熱補(bǔ)丁框架的分析


BsDiff;它格式無(wú)關(guān),但對(duì)Dex效果不是特別好,而且非常不穩(wěn)定。當(dāng)前微信對(duì)于so與部分資源,依然使用bsdiff算法;

DexMerge;它主要問(wèn)題在于合成時(shí)內(nèi)存占用過(guò)大,一個(gè)12M的dex,峰值內(nèi)存可能達(dá)到70多M;

DexDiff;通過(guò)深入Dex格式,實(shí)現(xiàn)一套diff差異小,內(nèi)存占用少以及支持增刪改的算法。

由于微信發(fā)布的Android_N混合編譯與對(duì)熱補(bǔ)丁影響解析,所以在tinker中完全使用了新的Dex,那樣既不出現(xiàn)Art地址錯(cuò)亂的問(wèn)題,在Dalvik也無(wú)須插樁。當(dāng)然考慮到補(bǔ)丁包的體積,我們不能直接將新的Dex放在里面。但我們可以將新舊兩個(gè)Dex的差異放到補(bǔ)丁包中。

整體的流程如下:

如何進(jìn)行Tinker Android熱補(bǔ)丁框架的分析


從流程圖來(lái)看,同樣可以很明顯的找到這種方式的特點(diǎn):

優(yōu)勢(shì):
合成整包,不用在構(gòu)造函數(shù)插入代碼,防止verify,verify和opt在編譯期間就已經(jīng)完成,不會(huì)在運(yùn)行期間進(jìn)行
性能提高。兼容性和穩(wěn)定性比較高。
開(kāi)發(fā)者透明,不需要對(duì)包進(jìn)行額外處理。
不足:
與超級(jí)補(bǔ)丁技術(shù)一樣,不支持即時(shí)生效,必須通過(guò)重啟應(yīng)用的方式才能生效。
需要給應(yīng)用開(kāi)啟新的進(jìn)程才能進(jìn)行合并,并且很容易因?yàn)閮?nèi)存消耗等原因合并失敗。
合并時(shí)占用額外磁盤空間,對(duì)于多DEX的應(yīng)用來(lái)說(shuō),如果修改了多個(gè)DEX文件,就需要下發(fā)多個(gè)patch.dex與對(duì)應(yīng)的classes.dex進(jìn)行合并操作時(shí)這種情況會(huì)更嚴(yán)重,因此合并過(guò)程的失敗率也會(huì)更高。

關(guān)于如何進(jìn)行Tinker Android熱補(bǔ)丁框架的分析就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺(jué)得文章不錯(cuò),可以把它分享出去讓更多的人看到。

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI