前言
熱修復技術是當下Android開發(fā)中比較高級和熱門的知識點,是中級開發(fā)人員通向高級開發(fā)中必須掌握的技能。同時目前Android業(yè)內,熱修復技術也是百花齊放,各大廠都推出了自己的熱修復方案,使用的技術方案也各有所異,當然各個方案也都存在各自的局限性。希望通過本文的梳理闡述,了解這些熱修復方案的對比及實現原理,掌握熱修復技術的本質,同時也能應用實踐到實際項目中去,幫助大家學以致用(文末有學習筆記分享)。
什么是熱修復
簡單來講,為了修復線上問題而提出的修補方案,程序修補過程無需重新發(fā)版!

正常版本開發(fā)與熱修復開發(fā)流程對比
為什么要學習熱修復
在正常軟件開發(fā)流程中,線下開發(fā)->上線->發(fā)現bug->緊急修復上線。不過對于這種方式代價太大,而且永遠避免不了面臨如下幾個問題:
開發(fā)上線的版本能保證不存在Bug么?
修復后的版本能保證用戶都及時更新么?
如何最大化減少線上Bug對業(yè)務的影響?
而相對比之下,熱修復的開發(fā)流程就顯得更加靈活,無需重新發(fā)版,實時高效熱修復,無需下載新的應用,代價小,最重要的是及時的修復了bug。而且隨著熱修復技術的發(fā)展,現在不僅可以修復代碼,同時還可以修復資源文件及SO庫。

怎么選擇合適的熱修復技術方案?
文章開篇就說了現在各大廠都推出了自己的熱修復方案,那么我們到底該如何去選擇一套適合自己的熱修復技術去學習呢?接下來我將從現在主流熱修復的方案對比來給予你答案。
國內主流熱修復技術方案
1、阿里系
名稱說明AndFix開源,實時生效HotFix阿里百川,未開源,免費、實時生效Sophix未開源,商業(yè)收費,實時生效/冷啟動修復
HotFix是AndFix的優(yōu)化版本,Sophix是HotFix的優(yōu)化版本。目前阿里系主推是Sophix。
2、騰訊系
名稱說明Qzone超級補丁QQ空間,未開源,冷啟動修復QFix手Q團隊,開源,冷啟動修復Tinker微信團隊,開源,冷啟動修復。提供分發(fā)管理,基礎版免費
3、其他
名稱說明Robust美團, 開源,實時修復Nuwa大眾點評,開源,冷啟動修復Amigo餓了么,開源,冷啟動修復
各熱修復方案對比

怎么選擇合適的熱修復方案 怎么選?這個只能說一切看需求。如果公司綜合實力強,完全考慮自研都沒問題,但需要綜合考慮成本及維護。下面給出2點建議,如下:
- 項目需求
只需要簡單的方法級別Bug修復?
需要資源及so庫的修復?
對平臺兼容性要求及成功率要求?
有需求對分發(fā)進行控制,對監(jiān)控數據進行統(tǒng)計,補丁包進行管理?
公司資源是否支持商業(yè)付費?
- 學習及使用成本
集成難度
代碼侵入性
調試維護
- 選擇大廠
技術性能有保障
有專人維護
熱度高,開源社區(qū)活躍
- 如果考慮付費,推薦選擇阿里的Sophix,Sophix是綜合優(yōu)化的產物,功能完善、開發(fā)簡單透明、提供分發(fā)及監(jiān)控管理。如果不考慮付費,只需支持方法級別的Bug修復,不支持資源及so,推薦使用Robust。如果考慮需要同時支持資源及so,推薦使用Tinker。最后如果公司綜合實力強,可考慮自研,靈活性及可控制最強。
熱修復技術方案原理
技術分類

image
NativeHook 原理
原理及實現
NativeHook的原理是直接在native層進行方法的結構體信息對換,從而實現完美的方法新舊替換,從而實現熱修復功能。 下面以AndFix的一段jni代碼來進行說明,如下:
<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函數對應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;
//把舊函數的所有成員變量都替換為新函數的
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中都對應一個ArtMethod,ArtMethod記錄了這個Java方法的所有信息,包括訪問權限及代碼執(zhí)行地址等。通過env->FromReflectedMethod得到方法對應的ArtMethod的真正開始地址,然后強轉為ArtMethod指針,從而對其所有成員進行修改。
這樣以后調用這個方法時就會直接走到新方法的實現中,達到熱修復的效果。
優(yōu)點
即時生效
沒有性能開銷,不需要任何編輯器的插樁或代碼改寫
缺點
存在穩(wěn)定及兼容性問題。ArtMethod的結構基本參考Google開源的代碼,各大廠商的ROM都可能有所改動,可能導致結構不一致,修復失敗。
無法增加變量及類,只能修復方法級別的Bug,無法做到新功能的發(fā)布
javaHook 原理
原理及實現
以美團的Robust為例,Robust 的原理可以簡單描述為:
1、打基礎包時插樁,在每個方法前插入一段類型為 ChangeQuickRedirect 靜態(tài)變量的邏輯,插入過程對業(yè)務開發(fā)是完全透明
2、加載補丁時,從補丁包中讀取要替換的類及具體替換的方法實現,新建ClassLoader加載補丁dex。當changeQuickRedirect不為null時,可能會執(zhí)行到accessDispatch從而替換掉之前老的邏輯,達到fix的目的

下面通過Robust的源碼來進行分析。 首先看一下打基礎包是插入的代碼邏輯,如下:
<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) {
//為每個方法自動插入修復邏輯代碼,如果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的核心修復源碼如下:
<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);
...
}
/**
- 應用補丁列表
/
protected void applyPatchList(List<Patch> patches) {
...
for (Patch p : patches) {
...
currentPatchResult = patch(context, p);
...
}
}
/* - 核心修復源碼
*/
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) {
...
}
...
//通過遍歷其中的類信息進而反射修改其中 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)點
高兼容性(Robust只是在正常的使用DexClassLoader)、高穩(wěn)定性,修復成功率高達99.9%
補丁實時生效,不需要重新啟動
支持方法級別的修復,包括靜態(tài)方法
支持增加方法和類
支持ProGuard的混淆、內聯、優(yōu)化等操作
缺點
代碼是侵入式的,會在原有的類中加入相關代碼
so和資源的替換暫時不支持
會增大apk的體積,平均一個函數會比原來增加17.47個字節(jié),10萬個函數會增加1.67M
java mulitdex 原理
原理及實現
Android內部使用的是BaseDexClassLoader、PathClassLoader、DexClassLoader三個類加載器實現從DEX文件中讀取類數據,其中PathClassLoader和DexClassLoader都是繼承自BaseDexClassLoader實現。dex文件轉換成dexFile對象,存入Element[]數組,findclass順序遍歷Element數組獲取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;
// 如果當前的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[],將補丁的dex插入到數組的最前端。因為ClassLoader的findClass是通過遍歷dexElements[]中的dex來尋找類的。所以會優(yōu)先查找到修復的類。從而達到修復的效果。

下面使用Nuwa的關鍵實現源碼進行說明如下:
<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加載補丁Dex
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader());
//反射獲取舊DexElements數組
Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
//反射獲取補丁DexElements數組
Object newDexElements = getDexElements(getPathList(dexClassLoader));
//合并,將新數組的Element插入到最前面
Object allDexElements = combineArray(newDexElements, baseDexElements);
Object pathList = getPathList(getPathClassLoader());
//更新舊ClassLoader中的Element數組
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)點
不需要考慮對dalvik虛擬機和art虛擬機做適配
代碼是非侵入式的,對apk體積影響不大
缺點
需要下次啟動才修復
性能損耗大,為了避免類被加上CLASS_ISPREVERIFIED,使用插樁,單獨放一個幫助類在獨立的dex中讓其他類調用。
dex替換
原理及實現
為了避免dex插樁帶來的性能損耗,dex替換采取另外的方式。原理是提供dex差量包,整體替換dex的方案。差量的方式給出patch.dex,然后將patch.dex與應用的classes.dex合并成一個完整的dex,完整dex加載得到dexFile對象作為參數構建一個Element對象然后整體替換掉舊的dex-Elements數組。

這也是微信Tinker采用的方案,并且Tinker自研了DexDiff/DexMerge算法。Tinker還支持資源和So包的更新,So補丁包使用BsDiff來生成,資源補丁包直接使用文件md5對比來生成,針對資源比較大的(默認大于100KB屬于大文件)會使用BsDiff來對文件生成差量補丁。
下面我們關鍵看看Tinker的實現源碼,當然具體的實現算法很復雜,我們只看關鍵的實現,最后的修復在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) {
//省略一堆校驗
... ....
?
//下面是關鍵的diff算法及合并實現,實現相對復雜,感興趣可以再仔細閱讀源碼
//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)點
兼容性高
補丁小
開發(fā)透明,代碼非侵入式
缺點
冷啟動修復,下次啟動修復
Dex合并內存消耗在vm head上,容易OOM,最后導致合并失敗
資源修復原理
Instant Run
1、構建一個新的AssetManager,并通過反射調用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的引用處,全部換成剛新構建的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修復原理
接口調用替換
sdk提供接口替換System默認加載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指定目錄下補丁的so。若不存在,則再去加載安裝apk目錄下的so庫
優(yōu)點:不需要對不同sdk版本進行兼容,所以sdk版本都是System.loadLibrary這個接口
缺點:需要侵入業(yè)務代碼,替換掉System默認加載so庫的接口
反射注入
采取類似類修復反射注入方式,只要把補丁so庫的路徑插入到nativeLibraryDirectories數組的最前面,就能夠達到加載so庫的時候是補丁so庫而不是原來so庫的目錄,從而達到修復。
<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)點:不需侵入用戶接口調用
缺點:需要做版本兼容控制,兼容性較差
使用熱修復技術有哪些需要注意的問題?
版本管理
使用熱修復技術后由于發(fā)布流程的變化,肯定也需求采用相應的分支管理進行控制。
通常移動開發(fā)的分支管理采用特性分支,如下:
分支描述master主分支(只能merge,不能commit,設置權限),用于管理線上版本,及時設置對應Tagdev開發(fā)分支,每個新版本的研發(fā)根據版本號基于主分支創(chuàng)建,測試通過驗證后,上線合入master分支function X功能分支,按需求設定。基于開發(fā)分支創(chuàng)建,完成功能開發(fā)后合入dev開發(fā)分支
接入熱修復后,推薦可參考如下分支策略:
分支描述master主分支(只能merge,不能commit,設置權限),用于管理線上版本,及時設置對應Tag(一般3位版本號)hot_fix熱修復分支。基于master分支創(chuàng)建,修復緊急問題后,測試推送后,將hot_fix再合并到master分支。再次為master分支打tag。(一般4位版本號)dev開發(fā)分支,每個新版本的研發(fā)根據版本號基于主分支創(chuàng)建,測試通過驗證后,上線合入master分支function X功能分支,按需求設定?;陂_發(fā)分支創(chuàng)建,完成功能開發(fā)后合入dev開發(fā)分支
注意熱修復分支的測試及發(fā)布流程應用正常版本流程一致,保證質量。
分發(fā)監(jiān)控
目前主流的熱修復方案,像Tinker及Sophix都會提供補丁的分發(fā)及監(jiān)控。這也是我們選擇熱修復技術方案需要考慮的關鍵因素之一。畢竟為了保證線上版本的質量,分發(fā)控制及實時監(jiān)測必不可少。
最后
想要深入了解熱修復,需要了解類加載機制,Instant Run,multidex以及java底層實現細節(jié),JNI,AAPT和虛擬機的知識,需要龐大的知識貯備才能進行深入理解,當然Android Framwork的實現細節(jié)是非常重要的。熟悉熱修復的原理有助于我們提供自己的編程水平,提升自己解決問題的能力,最后熱修復不是簡單的客戶端SDK,它還包含了安全機制和服務端的控制邏輯,整條鏈路也不是短時間可以快速完成的。
所以為了方便朋友們更直觀快速的學習掌握Android熱修復技術,我這里收集整理一套視頻+電子書的熱修復系列學習資料。視頻教程為愛奇藝高級工程師Lance老師主講,以Qzone熱修復實戰(zhàn)項目為例,深入淺出的全方位講解熱修復技術。電子書是來源于阿里的《深入探索Android熱修復技術原理》對熱修復技術有很深入解讀。
由于篇幅原因,這里只做一些截圖展示,需要完整資料的朋友,可以在點贊+評論后,后臺私信我免費獲??!
微信圖片_20201210160018.png
深入探索Android熱修復技術原理電子書

熱修復技術原理電子書內容
