前言
熱修復(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ā)版!
正常版本開發(fā)與熱修復(fù)開發(fā)流程對比
為什么要學(xué)習(xí)熱修復(fù)
在正常軟件開發(fā)流程中葫隙,線下開發(fā)->上線->發(fā)現(xiàn)bug->緊急修復(fù)上線栽烂。不過對于這種方式代價太大,而且永遠(yuǎn)避免不了面臨如下幾個問題:
開發(fā)上線的版本能保證不存在Bug么恋脚?
修復(fù)后的版本能保證用戶都及時更新么腺办?
如何最大化減少線上Bug對業(yè)務(wù)的影響?
而相對比之下慧起,熱修復(fù)的開發(fā)流程就顯得更加靈活菇晃,無需重新發(fā)版,實時高效熱修復(fù)蚓挤,無需下載新的應(yīng)用磺送,代價小,最重要的是及時的修復(fù)了bug灿意。而且隨著熱修復(fù)技術(shù)的發(fā)展估灿,現(xiàn)在不僅可以修復(fù)代碼,同時還可以修復(fù)資源文件及SO庫缤剧。
怎么選擇合適的熱修復(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ù)方案對比
怎么選擇合適的熱修復(fù)方案 怎么選链患?這個只能說一切看需求巧鸭。如果公司綜合實力強(qiáng),完全考慮自研都沒問題麻捻,但需要綜合考慮成本及維護(hù)纲仍。下面給出2點(diǎn)建議,如下:
- 項目需求
只需要簡單的方法級別Bug修復(fù)贸毕?
需要資源及so庫的修復(fù)郑叠?
對平臺兼容性要求及成功率要求?
有需求對分發(fā)進(jìn)行控制明棍,對監(jiān)控數(shù)據(jù)進(jìn)行統(tǒng)計乡革,補(bǔ)丁包進(jìn)行管理?
公司資源是否支持商業(yè)付費(fèi)摊腋?
- 學(xué)習(xí)及使用成本
集成難度
代碼侵入性
調(diào)試維護(hù)
- 選擇大廠
技術(shù)性能有保障
有專人維護(hù)
熱度高沸版,開源社區(qū)活躍
- 如果考慮付費(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ù)分類
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的目的
下面通過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ù)的效果煤墙。
下面使用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ù)組捺典。
這也是微信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)獲取喂走!
深入探索Android熱修復(fù)技術(shù)原理電子書
熱修復(fù)技術(shù)原理電子書內(nèi)容