從入門到精通,一文全解Android熱修復(fù)技術(shù)

前言

熱修復(fù)技術(shù)是當(dāng)下Android開發(fā)中比較高級和熱門的知識點(diǎn)绢慢,是中級開發(fā)人員通向高級開發(fā)中必須掌握的技能。同時目前Android業(yè)內(nèi),熱修復(fù)技術(shù)也是百花齊放胰舆,各大廠都推出了自己的熱修復(fù)方案骚露,使用的技術(shù)方案也各有所異,當(dāng)然各個方案也都存在各自的局限性缚窿。希望通過本文的梳理闡述棘幸,了解這些熱修復(fù)方案的對比及實現(xiàn)原理,掌握熱修復(fù)技術(shù)的本質(zhì)倦零,同時也能應(yīng)用實踐到實際項目中去误续,幫助大家學(xué)以致用(文末有學(xué)習(xí)筆記分享)。

什么是熱修復(fù)

簡單來講扫茅,為了修復(fù)線上問題而提出的修補(bǔ)方案蹋嵌,程序修補(bǔ)過程無需重新發(fā)版!

微信圖片_20201210155922.png

正常版本開發(fā)與熱修復(fù)開發(fā)流程對比

為什么要學(xué)習(xí)熱修復(fù)

在正常軟件開發(fā)流程中葫隙,線下開發(fā)->上線->發(fā)現(xiàn)bug->緊急修復(fù)上線栽烂。不過對于這種方式代價太大,而且永遠(yuǎn)避免不了面臨如下幾個問題:

  1. 開發(fā)上線的版本能保證不存在Bug么恋脚?

  2. 修復(fù)后的版本能保證用戶都及時更新么腺办?

  3. 如何最大化減少線上Bug對業(yè)務(wù)的影響?

而相對比之下慧起,熱修復(fù)的開發(fā)流程就顯得更加靈活菇晃,無需重新發(fā)版,實時高效熱修復(fù)蚓挤,無需下載新的應(yīng)用磺送,代價小,最重要的是及時的修復(fù)了bug灿意。而且隨著熱修復(fù)技術(shù)的發(fā)展估灿,現(xiàn)在不僅可以修復(fù)代碼,同時還可以修復(fù)資源文件及SO庫缤剧。

微信圖片_20201210155930.jpg

怎么選擇合適的熱修復(fù)技術(shù)方案馅袁?

文章開篇就說了現(xiàn)在各大廠都推出了自己的熱修復(fù)方案,那么我們到底該如何去選擇一套適合自己的熱修復(fù)技術(shù)去學(xué)習(xí)呢荒辕?接下來我將從現(xiàn)在主流熱修復(fù)的方案對比來給予你答案汗销。

國內(nèi)主流熱修復(fù)技術(shù)方案

1、阿里系

名稱說明AndFix開源抵窒,實時生效HotFix阿里百川弛针,未開源,免費(fèi)李皇、實時生效Sophix未開源削茁,商業(yè)收費(fèi),實時生效/冷啟動修復(fù)

HotFix是AndFix的優(yōu)化版本,Sophix是HotFix的優(yōu)化版本茧跋。目前阿里系主推是Sophix慰丛。

2、騰訊系

名稱說明Qzone超級補(bǔ)丁QQ空間瘾杭,未開源诅病,冷啟動修復(fù)QFix手Q團(tuán)隊,開源富寿,冷啟動修復(fù)Tinker微信團(tuán)隊睬隶,開源,冷啟動修復(fù)页徐。提供分發(fā)管理苏潜,基礎(chǔ)版免費(fèi)

3、其他

名稱說明Robust美團(tuán)变勇, 開源恤左,實時修復(fù)Nuwa大眾點(diǎn)評,開源搀绣,冷啟動修復(fù)Amigo餓了么飞袋,開源,冷啟動修復(fù)

各熱修復(fù)方案對比

微信圖片_20201210155936.png

怎么選擇合適的熱修復(fù)方案 怎么選链患?這個只能說一切看需求巧鸭。如果公司綜合實力強(qiáng),完全考慮自研都沒問題麻捻,但需要綜合考慮成本及維護(hù)纲仍。下面給出2點(diǎn)建議,如下:

  1. 項目需求
  • 只需要簡單的方法級別Bug修復(fù)贸毕?

  • 需要資源及so庫的修復(fù)郑叠?

  • 對平臺兼容性要求及成功率要求?

  • 有需求對分發(fā)進(jìn)行控制明棍,對監(jiān)控數(shù)據(jù)進(jìn)行統(tǒng)計乡革,補(bǔ)丁包進(jìn)行管理?

  • 公司資源是否支持商業(yè)付費(fèi)摊腋?

  1. 學(xué)習(xí)及使用成本
  • 集成難度

  • 代碼侵入性

  • 調(diào)試維護(hù)

  1. 選擇大廠
  • 技術(shù)性能有保障

  • 有專人維護(hù)

  • 熱度高沸版,開源社區(qū)活躍

  1. 如果考慮付費(fèi),推薦選擇阿里的Sophix兴蒸,Sophix是綜合優(yōu)化的產(chǎn)物推穷,功能完善、開發(fā)簡單透明类咧、提供分發(fā)及監(jiān)控管理。如果不考慮付費(fèi),只需支持方法級別的Bug修復(fù)痕惋,不支持資源及so区宇,推薦使用Robust。如果考慮需要同時支持資源及so值戳,推薦使用Tinker议谷。最后如果公司綜合實力強(qiáng),可考慮自研堕虹,靈活性及可控制最強(qiáng)卧晓。

熱修復(fù)技術(shù)方案原理

技術(shù)分類

微信圖片_20201210155941.jpg

image

NativeHook 原理

原理及實現(xiàn)

NativeHook的原理是直接在native層進(jìn)行方法的結(jié)構(gòu)體信息對換,從而實現(xiàn)完美的方法新舊替換赴捞,從而實現(xiàn)熱修復(fù)功能逼裆。 下面以AndFix的一段jni代碼來進(jìn)行說明,如下:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n79" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">void replace_6_0(JNIEnv* env, jobject src, jobject dest) {
?
// 通過Method對象得到底層Java函數(shù)對應(yīng)ArtMethod的真實地址
art::mirror::ArtMethod* smeth =
(art::mirror::ArtMethod) env->FromReflectedMethod(src);
?
art::mirror::ArtMethod
dmeth =
(art::mirror::ArtMethod) env->FromReflectedMethod(dest);
?
reinterpret_cast<art::mirror::Class
>(dmeth->declaring_class_)->class_loader_ =
reinterpret_cast<art::mirror::Class>(smeth->declaring_class_)->class_loader_; //for plugin classloader
reinterpret_cast<art::mirror::Class
>(dmeth->declaring_class_)->clinit_thread_id_ =
reinterpret_cast<art::mirror::Class>(smeth->declaring_class_)->clinit_thread_id_;
reinterpret_cast<art::mirror::Class
>(dmeth->declaring_class_)->status_ = reinterpret_cast<art::mirror::Class>(smeth->declaring_class_)->status_-1;
//for reflection invoke
reinterpret_cast<art::mirror::Class
>(dmeth->declaring_class_)->super_class_ = 0;
//把舊函數(shù)的所有成員變量都替換為新函數(shù)的
smeth->declaring_class_ = dmeth->declaring_class_;
smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
smeth->access_flags_ = dmeth->access_flags_ | 0x0001;
smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;
smeth->dex_method_index_ = dmeth->dex_method_index_;
smeth->method_index_ = dmeth->method_index_;
?
smeth->ptr_sized_fields_.entry_point_from_interpreter_ =
dmeth->ptr_sized_fields_.entry_point_from_interpreter_;
?
smeth->ptr_sized_fields_.entry_point_from_jni_ =
dmeth->ptr_sized_fields_.entry_point_from_jni_;
smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =
dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;
?
LOGD("replace_6_0: %d , %d",
smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_,
dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_);
}
?
void setFieldFlag_6_0(JNIEnv* env, jobject field) {
art::mirror::ArtField* artField =
(art::mirror::ArtField*) env->FromReflectedField(field);
artField->access_flags_ = artField->access_flags_ & (~0x0002) | 0x0001;
LOGD("setFieldFlag_6_0: %d ", artField->access_flags_);
}</pre>

每一個Java方法在art中都對應(yīng)一個ArtMethod,ArtMethod記錄了這個Java方法的所有信息赦政,包括訪問權(quán)限及代碼執(zhí)行地址等胜宇。通過env->FromReflectedMethod得到方法對應(yīng)的ArtMethod的真正開始地址,然后強(qiáng)轉(zhuǎn)為ArtMethod指針恢着,從而對其所有成員進(jìn)行修改桐愉。

這樣以后調(diào)用這個方法時就會直接走到新方法的實現(xiàn)中,達(dá)到熱修復(fù)的效果掰派。

優(yōu)點(diǎn)

  • 即時生效

  • 沒有性能開銷从诲,不需要任何編輯器的插樁或代碼改寫

缺點(diǎn)

  • 存在穩(wěn)定及兼容性問題。ArtMethod的結(jié)構(gòu)基本參考Google開源的代碼靡羡,各大廠商的ROM都可能有所改動系洛,可能導(dǎo)致結(jié)構(gòu)不一致,修復(fù)失敗亿眠。

  • 無法增加變量及類碎罚,只能修復(fù)方法級別的Bug,無法做到新功能的發(fā)布

javaHook 原理

原理及實現(xiàn)

以美團(tuán)的Robust為例纳像,Robust 的原理可以簡單描述為:

1荆烈、打基礎(chǔ)包時插樁,在每個方法前插入一段類型為 ChangeQuickRedirect 靜態(tài)變量的邏輯竟趾,插入過程對業(yè)務(wù)開發(fā)是完全透明

2憔购、加載補(bǔ)丁時,從補(bǔ)丁包中讀取要替換的類及具體替換的方法實現(xiàn)岔帽,新建ClassLoader加載補(bǔ)丁dex玫鸟。當(dāng)changeQuickRedirect不為null時,可能會執(zhí)行到accessDispatch從而替換掉之前老的邏輯犀勒,達(dá)到fix的目的

微信圖片_20201210155946.jpg

下面通過Robust的源碼來進(jìn)行分析屎飘。 首先看一下打基礎(chǔ)包是插入的代碼邏輯妥曲,如下:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n102" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public static ChangeQuickRedirect u;
protected void onCreate(Bundle bundle) {
//為每個方法自動插入修復(fù)邏輯代碼,如果ChangeQuickRedirect為空則不執(zhí)行
if (u != null) {
if (PatchProxy.isSupport(new Object[]{bundle}, this, u, false, 78)) {
PatchProxy.accessDispatchVoid(new Object[]{bundle}, this, u, false, 78);
return;
}
}
super.onCreate(bundle);
...
}</pre>

Robust的核心修復(fù)源碼如下:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n104" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public class PatchExecutor extends Thread {
@Override
public void run() {
...
applyPatchList(patches);
...
}
/**

  • 應(yīng)用補(bǔ)丁列表
    /
    protected void applyPatchList(List<Patch> patches) {
    ...
    for (Patch p : patches) {
    ...
    currentPatchResult = patch(context, p);
    ...
    }
    }
    /
    *
  • 核心修復(fù)源碼
    */
    protected boolean patch(Context context, Patch patch) {
    ...
    //新建ClassLoader
    DexClassLoader classLoader = new DexClassLoader(patch.getTempPath(), context.getCacheDir().getAbsolutePath(),
    null, PatchExecutor.class.getClassLoader());
    patch.delete(patch.getTempPath());
    ...
    try {
    patchsInfoClass = classLoader.loadClass(patch.getPatchesInfoImplClassFullName());
    patchesInfo = (PatchesInfo) patchsInfoClass.newInstance();
    } catch (Throwable t) {
    ...
    }
    ...
    //通過遍歷其中的類信息進(jìn)而反射修改其中 ChangeQuickRedirect 對象的值
    for (PatchedClassInfo patchedClassInfo : patchedClasses) {
    ...
    try {
    oldClass = classLoader.loadClass(patchedClassName.trim());
    Field[] fields = oldClass.getDeclaredFields();
    for (Field field : fields) {
    if (TextUtils.equals(field.getType().getCanonicalName(), ChangeQuickRedirect.class.getCanonicalName()) && TextUtils.equals(field.getDeclaringClass().getCanonicalName(), oldClass.getCanonicalName())) {
    changeQuickRedirectField = field;
    break;
    }
    }
    ...
    try {
    patchClass = classLoader.loadClass(patchClassName);
    Object patchObject = patchClass.newInstance();
    changeQuickRedirectField.setAccessible(true);
    changeQuickRedirectField.set(null, patchObject);
    } catch (Throwable t) {
    ...
    }
    } catch (Throwable t) {
    ...
    }
    }
    return true;
    }
    }</pre>

優(yōu)點(diǎn)

  • 高兼容性(Robust只是在正常的使用DexClassLoader)钦购、高穩(wěn)定性檐盟,修復(fù)成功率高達(dá)99.9%

  • 補(bǔ)丁實時生效,不需要重新啟動

  • 支持方法級別的修復(fù)押桃,包括靜態(tài)方法

  • 支持增加方法和類

  • 支持ProGuard的混淆葵萎、內(nèi)聯(lián)、優(yōu)化等操作

缺點(diǎn)

  • 代碼是侵入式的唱凯,會在原有的類中加入相關(guān)代碼

  • so和資源的替換暫時不支持

  • 會增大apk的體積羡忘,平均一個函數(shù)會比原來增加17.47個字節(jié),10萬個函數(shù)會增加1.67M

java mulitdex 原理

原理及實現(xiàn)

Android內(nèi)部使用的是BaseDexClassLoader磕昼、PathClassLoader卷雕、DexClassLoader三個類加載器實現(xiàn)從DEX文件中讀取類數(shù)據(jù),其中PathClassLoader和DexClassLoader都是繼承自BaseDexClassLoader實現(xiàn)掰烟。dex文件轉(zhuǎn)換成dexFile對象爽蝴,存入Element[]數(shù)組,findclass順序遍歷Element數(shù)組獲取DexFile纫骑,然后執(zhí)行DexFile的findclass蝎亚。源碼如下:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n128" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">// 加載名字為name的class對象
public Class findClass(String name, List<Throwable> suppressed) {
// 遍歷從dexPath查詢到的dex和資源Element
for (Element element : dexElements) {
DexFile dex = element.dexFile;
// 如果當(dāng)前的Element是dex文件元素
if (dex != null) {
// 使用DexFile.loadClassBinaryName加載類
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}</pre>

所以此方案的原理是Hook了ClassLoader.pathList.dexElements[],將補(bǔ)丁的dex插入到數(shù)組的最前端先馆。因為ClassLoader的findClass是通過遍歷dexElements[]中的dex來尋找類的发框。所以會優(yōu)先查找到修復(fù)的類。從而達(dá)到修復(fù)的效果煤墙。

微信圖片_20201210155951.jpg

下面使用Nuwa的關(guān)鍵實現(xiàn)源碼進(jìn)行說明如下:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n133" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public static void injectDexAtFirst(String dexPath, String defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
//新建一個ClassLoader加載補(bǔ)丁Dex
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader());
//反射獲取舊DexElements數(shù)組
Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
//反射獲取補(bǔ)丁DexElements數(shù)組
Object newDexElements = getDexElements(getPathList(dexClassLoader));
//合并梅惯,將新數(shù)組的Element插入到最前面
Object allDexElements = combineArray(newDexElements, baseDexElements);
Object pathList = getPathList(getPathClassLoader());
//更新舊ClassLoader中的Element數(shù)組
ReflectionUtils.setField(pathList, pathList.getClass(), "dexElements", allDexElements);
}
?
private static PathClassLoader getPathClassLoader() {
PathClassLoader pathClassLoader = (PathClassLoader) DexUtils.class.getClassLoader();
return pathClassLoader;
}
?
private static Object getDexElements(Object paramObject)
throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
return ReflectionUtils.getField(paramObject, paramObject.getClass(), "dexElements");
}
?
private static Object getPathList(Object baseDexClassLoader)
throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
return ReflectionUtils.getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
}
?
private static Object combineArray(Object firstArray, Object secondArray) {
Class<?> localClass = firstArray.getClass().getComponentType();
int firstArrayLength = Array.getLength(firstArray);
int allLength = firstArrayLength + Array.getLength(secondArray);
Object result = Array.newInstance(localClass, allLength);
for (int k = 0; k < allLength; ++k) {
if (k < firstArrayLength) {
Array.set(result, k, Array.get(firstArray, k));
} else {
Array.set(result, k, Array.get(secondArray, k - firstArrayLength));
}
}
return result;
}</pre>

優(yōu)點(diǎn)

  • 不需要考慮對dalvik虛擬機(jī)和art虛擬機(jī)做適配

  • 代碼是非侵入式的,對apk體積影響不大

缺點(diǎn)

  • 需要下次啟動才修復(fù)

  • 性能損耗大仿野,為了避免類被加上CLASS_ISPREVERIFIED铣减,使用插樁,單獨(dú)放一個幫助類在獨(dú)立的dex中讓其他類調(diào)用脚作。

dex替換

原理及實現(xiàn)

為了避免dex插樁帶來的性能損耗葫哗,dex替換采取另外的方式。原理是提供dex差量包球涛,整體替換dex的方案劣针。差量的方式給出patch.dex,然后將patch.dex與應(yīng)用的classes.dex合并成一個完整的dex亿扁,完整dex加載得到dexFile對象作為參數(shù)構(gòu)建一個Element對象然后整體替換掉舊的dex-Elements數(shù)組捺典。

微信圖片_20201210160001.jpg

這也是微信Tinker采用的方案干旁,并且Tinker自研了DexDiff/DexMerge算法部念。Tinker還支持資源和So包的更新腻豌,So補(bǔ)丁包使用BsDiff來生成厦画,資源補(bǔ)丁包直接使用文件md5對比來生成,針對資源比較大的(默認(rèn)大于100KB屬于大文件)會使用BsDiff來對文件生成差量補(bǔ)丁稀蟋。

下面我們關(guān)鍵看看Tinker的實現(xiàn)源碼煌张,當(dāng)然具體的實現(xiàn)算法很復(fù)雜,我們只看關(guān)鍵的實現(xiàn)退客,最后的修復(fù)在UpgradePatch中的tryPatch方法,如下:

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n153" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> @Override
public boolean tryPatch(Context context, String tempPatchPath, PatchResult patchResult) {
//省略一堆校驗
... ....
?
//下面是關(guān)鍵的diff算法及合并實現(xiàn)链嘀,實現(xiàn)相對復(fù)雜萌狂,感興趣可以再仔細(xì)閱讀源碼
//we use destPatchFile instead of patchFile, because patchFile may be deleted during the patch process
if (!DexDiffPatchInternal.tryRecoverDexFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch dex failed");
return false;
}
?
if (!BsDiffPatchInternal.tryRecoverLibraryFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch library failed");
return false;
}
?
if (!ResDiffPatchInternal.tryRecoverResourceFiles(manager, signatureCheck, context, patchVersionDirectory, destPatchFile)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, try patch resource failed");
return false;
}
?
// check dex opt file at last, some phone such as VIVO/OPPO like to change dex2oat to interpreted
if (!DexDiffPatchInternal.waitAndCheckDexOptFile(patchFile, manager)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, check dex opt file failed");
return false;
}
?
if (!SharePatchInfo.rewritePatchInfoFileWithLock(patchInfoFile, newInfo, patchInfoLockFile)) {
TinkerLog.e(TAG, "UpgradePatch tryPatch:new patch recover, rewrite patch info failed");
manager.getPatchReporter().onPatchInfoCorrupted(patchFile, newInfo.oldVersion, newInfo.newVersion);
return false;
}
?
TinkerLog.w(TAG, "UpgradePatch tryPatch: done, it is ok");
return true;
}</pre>

優(yōu)點(diǎn)

  • 兼容性高

  • 補(bǔ)丁小

  • 開發(fā)透明,代碼非侵入式

缺點(diǎn)

  • 冷啟動修復(fù)怀泊,下次啟動修復(fù)

  • Dex合并內(nèi)存消耗在vm head上茫藏,容易OOM,最后導(dǎo)致合并失敗

資源修復(fù)原理

Instant Run

1霹琼、構(gòu)建一個新的AssetManager务傲,并通過反射調(diào)用addAssertPath,把這個完整的新資源包加入到AssetManager中枣申。這樣就得到一個含有所有新資源的AssetManager

2售葡、找到所有值錢引用到原有AssetManager的地方,通過反射忠藤,把引用處替換為AssetManager

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n172" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;"> public static void monkeyPatchExistingResources(Context context,
String externalResourceFile, Collection activities) {
if (externalResourceFile == null) {
return;
}
try {
//反射一個新的 AssetManager
AssetManager newAssetManager = (AssetManager) AssetManager.class
.getConstructor(new Class[0]).newInstance(new Object[0]);
//反射 addAssetPath 添加新的資源包
Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", new Class[]{String.class});
mAddAssetPath.setAccessible(true);
if (((Integer) mAddAssetPath.invoke(newAssetManager,
new Object[]{externalResourceFile})).intValue() == 0) {
throw new IllegalStateException(
"Could not create new AssetManager");
}
Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks", new Class[0]);
mEnsureStringBlocks.setAccessible(true);
mEnsureStringBlocks.invoke(newAssetManager, new Object[0]);
//反射得到Activity中AssetManager的引用處挟伙,全部換成剛新構(gòu)建的AssetManager對象
if (activities != null) {
for (Activity activity : activities) {
Resources resources = activity.getResources();
try {
Field mAssets = Resources.class.getDeclaredField("mAssets");
mAssets.setAccessible(true);
mAssets.set(resources, newAssetManager);
} catch (Throwable ignore) {
Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
mResourcesImpl.setAccessible(true);
Object resourceImpl = mResourcesImpl.get(resources);
Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
implAssets.setAccessible(true);
implAssets.set(resourceImpl, newAssetManager);
}
Resources.Theme theme = activity.getTheme();
try {
try {
Field ma = Resources.Theme.class.getDeclaredField("mAssets");
ma.setAccessible(true);
ma.set(theme, newAssetManager);
} catch (NoSuchFieldException ignore) {
Field themeField = Resources.Theme.class.getDeclaredField("mThemeImpl");
themeField.setAccessible(true);
Object impl = themeField.get(theme);
Field ma = impl.getClass().getDeclaredField("mAssets");
ma.setAccessible(true);
ma.set(impl, newAssetManager);
}
Field mt = ContextThemeWrapper.class.getDeclaredField("mTheme");
mt.setAccessible(true);
mt.set(activity, null);
Method mtm = ContextThemeWrapper.class.getDeclaredMethod("initializeTheme", new Class[0]);
mtm.setAccessible(true);
mtm.invoke(activity, new Object[0]);
Method mCreateTheme = AssetManager.class.getDeclaredMethod("createTheme", new Class[0]);
mCreateTheme.setAccessible(true);
Object internalTheme = mCreateTheme.invoke(newAssetManager, new Object[0]);
Field mTheme = Resources.Theme.class.getDeclaredField("mTheme");
mTheme.setAccessible(true);
mTheme.set(theme, internalTheme);
} catch (Throwable e) {
Log.e("InstantRun",
"Failed to update existing theme for activity "

  • activity, e);
    }
    pruneResourceCaches(resources);
    }
    }
    Collection references;
    if (Build.VERSION.SDK_INT >= 19) {
    Class resourcesManagerClass = Class.forName("android.app.ResourcesManager");
    Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance", new Class[0]);
    mGetInstance.setAccessible(true);
    Object resourcesManager = mGetInstance.invoke(null, new Object[0]);
    try {
    Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources");
    fMActiveResources.setAccessible(true);
    ArrayMap arrayMap = (ArrayMap) fMActiveResources.get(resourcesManager);
    references = arrayMap.values();
    } catch (NoSuchFieldException ignore) {
    Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences");
    mResourceReferences.setAccessible(true);
    references = (Collection) mResourceReferences.get(resourcesManager);
    }
    } else {
    Class activityThread = Class.forName("android.app.ActivityThread");
    Field fMActiveResources = activityThread.getDeclaredField("mActiveResources");
    fMActiveResources.setAccessible(true);
    Object thread = getActivityThread(context, activityThread);
    HashMap map = (HashMap) fMActiveResources.get(thread);
    references = map.values();
    }
    for (WeakReference wr : references) {
    Resources resources = (Resources) wr.get();
    if (resources != null) {
    try {
    Field mAssets = Resources.class.getDeclaredField("mAssets");
    mAssets.setAccessible(true);
    mAssets.set(resources, newAssetManager);
    } catch (Throwable ignore) {
    Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
    mResourcesImpl.setAccessible(true);
    Object resourceImpl = mResourcesImpl.get(resources);
    Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
    implAssets.setAccessible(true);
    implAssets.set(resourceImpl, newAssetManager);
    }
    resources.updateConfiguration(resources.getConfiguration(), resources.getDisplayMetrics());
    }
    }
    } catch (Throwable e) {
    throw new IllegalStateException(e);
    }
    }</pre>

so修復(fù)原理

接口調(diào)用替換

sdk提供接口替換System默認(rèn)加載so庫的接口

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n176" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">SOPatchManger.loadLibrary(String libName)
替換
System.loadLibrary(String libName)</pre>

SOPatchManger.loadLibrary接口加載so庫的時候優(yōu)先嘗試去加載sdk指定目錄下補(bǔ)丁的so。若不存在模孩,則再去加載安裝apk目錄下的so庫

優(yōu)點(diǎn):不需要對不同sdk版本進(jìn)行兼容尖阔,所以sdk版本都是System.loadLibrary這個接口

缺點(diǎn):需要侵入業(yè)務(wù)代碼,替換掉System默認(rèn)加載so庫的接口

反射注入

采取類似類修復(fù)反射注入方式榨咐,只要把補(bǔ)丁so庫的路徑插入到nativeLibraryDirectories數(shù)組的最前面介却,就能夠達(dá)到加載so庫的時候是補(bǔ)丁so庫而不是原來so庫的目錄,從而達(dá)到修復(fù)块茁。

<pre spellcheck="false" class="md-fences md-end-block ty-contain-cm modeLoaded" lang="" cid="n182" mdtype="fences" style="box-sizing: border-box; overflow: visible; font-family: var(--monospace); font-size: 0.9em; display: block; break-inside: avoid; text-align: left; white-space: normal; background-image: inherit; background-position: inherit; background-size: inherit; background-repeat: inherit; background-attachment: inherit; background-origin: inherit; background-clip: inherit; background-color: rgb(248, 248, 248); position: relative !important; border: 1px solid rgb(231, 234, 237); border-radius: 3px; padding: 8px 4px 6px; margin-bottom: 15px; margin-top: 15px; width: inherit; color: rgb(51, 51, 51); font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; text-decoration-style: initial; text-decoration-color: initial;">public String findLibrary(String libraryName) {
String fileName = System.mapLibraryName(libraryName);
?
for (NativeLibraryElement element : nativeLibraryPathElements) {
String path = element.findNativeLibrary(fileName);
?
if (path != null) {
return path;
}
}
?
return null;
}</pre>

優(yōu)點(diǎn):不需侵入用戶接口調(diào)用

缺點(diǎn):需要做版本兼容控制齿坷,兼容性較差

使用熱修復(fù)技術(shù)有哪些需要注意的問題?

版本管理

使用熱修復(fù)技術(shù)后由于發(fā)布流程的變化龟劲,肯定也需求采用相應(yīng)的分支管理進(jìn)行控制胃夏。

通常移動開發(fā)的分支管理采用特性分支,如下:

分支描述master主分支(只能merge昌跌,不能commit仰禀,設(shè)置權(quán)限),用于管理線上版本蚕愤,及時設(shè)置對應(yīng)Tagdev開發(fā)分支答恶,每個新版本的研發(fā)根據(jù)版本號基于主分支創(chuàng)建饺蚊,測試通過驗證后,上線合入master分支function X功能分支悬嗓,按需求設(shè)定污呼。基于開發(fā)分支創(chuàng)建包竹,完成功能開發(fā)后合入dev開發(fā)分支

接入熱修復(fù)后燕酷,推薦可參考如下分支策略:

分支描述master主分支(只能merge,不能commit周瞎,設(shè)置權(quán)限)苗缩,用于管理線上版本,及時設(shè)置對應(yīng)Tag(一般3位版本號)hot_fix熱修復(fù)分支声诸〗囱龋基于master分支創(chuàng)建,修復(fù)緊急問題后彼乌,測試推送后泻肯,將hot_fix再合并到master分支。再次為master分支打tag慰照。(一般4位版本號)dev開發(fā)分支灶挟,每個新版本的研發(fā)根據(jù)版本號基于主分支創(chuàng)建,測試通過驗證后焚挠,上線合入master分支function X功能分支膏萧,按需求設(shè)定◎蛳危基于開發(fā)分支創(chuàng)建榛泛,完成功能開發(fā)后合入dev開發(fā)分支

注意熱修復(fù)分支的測試及發(fā)布流程應(yīng)用正常版本流程一致,保證質(zhì)量噩斟。

分發(fā)監(jiān)控

目前主流的熱修復(fù)方案曹锨,像Tinker及Sophix都會提供補(bǔ)丁的分發(fā)及監(jiān)控。這也是我們選擇熱修復(fù)技術(shù)方案需要考慮的關(guān)鍵因素之一剃允。畢竟為了保證線上版本的質(zhì)量沛简,分發(fā)控制及實時監(jiān)測必不可少。

最后

想要深入了解熱修復(fù)斥废,需要了解類加載機(jī)制椒楣,Instant Run,multidex以及java底層實現(xiàn)細(xì)節(jié)牡肉,JNI捧灰,AAPT和虛擬機(jī)的知識,需要龐大的知識貯備才能進(jìn)行深入理解统锤,當(dāng)然Android Framwork的實現(xiàn)細(xì)節(jié)是非常重要的毛俏。熟悉熱修復(fù)的原理有助于我們提供自己的編程水平炭庙,提升自己解決問題的能力,最后熱修復(fù)不是簡單的客戶端SDK煌寇,它還包含了安全機(jī)制和服務(wù)端的控制邏輯焕蹄,整條鏈路也不是短時間可以快速完成的。

所以為了方便朋友們更直觀快速的學(xué)習(xí)掌握Android熱修復(fù)技術(shù)阀溶,我這里收集整理一套視頻+電子書的熱修復(fù)系列學(xué)習(xí)資料腻脏。視頻教程為愛奇藝高級工程師Lance老師主講,以Qzone熱修復(fù)實戰(zhàn)項目為例银锻,深入淺出的全方位講解熱修復(fù)技術(shù)迹卢。電子書是來源于阿里的《深入探索Android熱修復(fù)技術(shù)原理》對熱修復(fù)技術(shù)有很深入解讀。

由于篇幅原因徒仓,這里只做一些截圖展示,需要完整資料的朋友誊垢,可以在點(diǎn)贊+評論后掉弛,后臺私信我免費(fèi)獲取喂走!


微信圖片_20201210160018.png

深入探索Android熱修復(fù)技術(shù)原理電子書

微信圖片_20201210160023.png

熱修復(fù)技術(shù)原理電子書內(nèi)容

需要完整資料的朋友殃饿,可以在點(diǎn)贊+評論后,后臺私信我免費(fèi)獲扔蟪Α乎芳!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市帖池,隨后出現(xiàn)的幾起案子奈惑,更是在濱河造成了極大的恐慌,老刑警劉巖睡汹,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件肴甸,死亡現(xiàn)場離奇詭異,居然都是意外死亡囚巴,警方通過查閱死者的電腦和手機(jī)原在,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來彤叉,“玉大人庶柿,你說我怎么就攤上這事』嘟剑” “怎么了浮庐?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長兼呵。 經(jīng)常有香客問我兔辅,道長腊敲,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任维苔,我火速辦了婚禮碰辅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘介时。我一直安慰自己没宾,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布沸柔。 她就那樣靜靜地躺著循衰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪褐澎。 梳的紋絲不亂的頭發(fā)上会钝,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天,我揣著相機(jī)與錄音工三,去河邊找鬼迁酸。 笑死,一個胖子當(dāng)著我的面吹牛俭正,可吹牛的內(nèi)容都是我干的奸鬓。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼掸读,長吁一口氣:“原來是場噩夢啊……” “哼串远!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起儿惫,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤澡罚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后姥闪,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體始苇,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年筐喳,在試婚紗的時候發(fā)現(xiàn)自己被綠了催式。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡避归,死狀恐怖荣月,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情梳毙,我是刑警寧澤哺窄,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響萌业,放射性物質(zhì)發(fā)生泄漏坷襟。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一生年、第九天 我趴在偏房一處隱蔽的房頂上張望婴程。 院中可真熱鬧,春花似錦抱婉、人聲如沸档叔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽衙四。三九已至,卻和暖如春患亿,著一層夾襖步出監(jiān)牢的瞬間传蹈,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工步藕, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留卡睦,地道東北人。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓漱抓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親恕齐。 傳聞我的和親對象是個殘疾皇子乞娄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評論 2 345

推薦閱讀更多精彩內(nèi)容