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

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

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

怎么選擇合適的熱修復(fù)方案
怎么選?這個(gè)只能說一切看需求。如果公司綜合實(shí)力強(qiáng),完全考慮自研都沒問題,但需要綜合考慮成本及維護(hù)。下面給出2點(diǎn)建議,如下:
- 項(xiàng)目需求
- 只需要簡單的方法級別Bug修復(fù)?
- 需要資源及so庫的修復(fù)?
- 對平臺兼容性要求及成功率要求?
- 有需求對分發(fā)進(jìn)行控制,對監(jiān)控?cái)?shù)據(jù)進(jìn)行統(tǒng)計(jì),補(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。如果考慮需要同時(shí)支持資源及so,推薦使用Tinker。最后如果公司綜合實(shí)力強(qiáng),可考慮自研,靈活性及可控制最強(qiáng)。
熱修復(fù)技術(shù)方案原理
技術(shù)分類
NativeHook 原理
原理及實(shí)現(xiàn)
NativeHook的原理是直接在native層進(jìn)行方法的結(jié)構(gòu)體信息對換,從而實(shí)現(xiàn)完美的方法新舊替換,從而實(shí)現(xiàn)熱修復(fù)功能。
下面以AndFix的一段jni代碼來進(jìn)行說明,如下:
void replace_6_0(JNIEnv* env, jobject src, jobject dest) {
// 通過Method對象得到底層Java函數(shù)對應(yīng)ArtMethod的真實(shí)地址
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_);
}
每一個(gè)Java方法在art中都對應(yīng)一個(gè)ArtMethod,ArtMethod記錄了這個(gè)Java方法的所有信息,包括訪問權(quán)限及代碼執(zhí)行地址等。通過env->FromReflectedMethod得到方法對應(yīng)的ArtMethod的真正開始地址,然后強(qiáng)轉(zhuǎn)為ArtMethod指針,從而對其所有成員進(jìn)行修改。
這樣以后調(diào)用這個(gè)方法時(shí)就會直接走到新方法的實(shí)現(xiàn)中,達(dá)到熱修復(fù)的效果。
優(yōu)點(diǎn)
- 即時(shí)生效
- 沒有性能開銷,不需要任何編輯器的插樁或代碼改寫
缺點(diǎn)
- 存在穩(wěn)定及兼容性問題。ArtMethod的結(jié)構(gòu)基本參考Google開源的代碼,各大廠商的ROM都可能有所改動,可能導(dǎo)致結(jié)構(gòu)不一致,修復(fù)失敗。
- 無法增加變量及類,只能修復(fù)方法級別的Bug,無法做到新功能的發(fā)布
javaHook 原理
原理及實(shí)現(xiàn)
以美團(tuán)的Robust為例,Robust 的原理可以簡單描述為:
1、打基礎(chǔ)包時(shí)插樁,在每個(gè)方法前插入一段類型為 ChangeQuickRedirect 靜態(tài)變量的邏輯,插入過程對業(yè)務(wù)開發(fā)是完全透明
2、加載補(bǔ)丁時(shí),從補(bǔ)丁包中讀取要替換的類及具體替換的方法實(shí)現(xiàn),新建ClassLoader加載補(bǔ)丁dex。當(dāng)changeQuickRedirect不為null時(shí),可能會執(zhí)行到accessDispatch從而替換掉之前老的邏輯,達(dá)到fix的目的

下面通過Robust的源碼來進(jìn)行分析。
首先看一下打基礎(chǔ)包是插入的代碼邏輯,如下:
public static ChangeQuickRedirect u;
protected void onCreate(Bundle bundle) {
//為每個(gè)方法自動插入修復(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);
...
}
Robust的核心修復(fù)源碼如下:
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;
}
}
優(yōu)點(diǎn)
- 高兼容性(Robust只是在正常的使用DexClassLoader)、高穩(wěn)定性,修復(fù)成功率高達(dá)99.9%
- 補(bǔ)丁實(shí)時(shí)生效,不需要重新啟動
- 支持方法級別的修復(fù),包括靜態(tài)方法
- 支持增加方法和類
- 支持ProGuard的混淆、內(nèi)聯(lián)、優(yōu)化等操作
缺點(diǎn)
- 代碼是侵入式的,會在原有的類中加入相關(guān)代碼
- so和資源的替換暫時(shí)不支持
- 會增大apk的體積,平均一個(gè)函數(shù)會比原來增加17.47個(gè)字節(jié),10萬個(gè)函數(shù)會增加1.67M
java mulitdex 原理
原理及實(shí)現(xiàn)
Android內(nèi)部使用的是BaseDexClassLoader、PathClassLoader、DexClassLoader三個(gè)類加載器實(shí)現(xiàn)從DEX文件中讀取類數(shù)據(jù),其中PathClassLoader和DexClassLoader都是繼承自BaseDexClassLoader實(shí)現(xiàn)。dex文件轉(zhuǎn)換成dexFile對象,存入Element[]數(shù)組,findclass順序遍歷Element數(shù)組獲取DexFile,然后執(zhí)行DexFile的findclass。源碼如下:
// 加載名字為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;
}
所以此方案的原理是Hook了ClassLoader.pathList.dexElements[],將補(bǔ)丁的dex插入到數(shù)組的最前端。因?yàn)镃lassLoader的findClass是通過遍歷dexElements[]中的dex來尋找類的。所以會優(yōu)先查找到修復(fù)的類。從而達(dá)到修復(fù)的效果。
下面使用Nuwa的關(guān)鍵實(shí)現(xiàn)源碼進(jìn)行說明如下:
public static void injectDexAtFirst(String dexPath, String defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
//新建一個(gè)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;
}
優(yōu)點(diǎn)
- 不需要考慮對dalvik虛擬機(jī)和art虛擬機(jī)做適配
- 代碼是非侵入式的,對apk體積影響不大
缺點(diǎn)
- 需要下次啟動才修復(fù)
- 性能損耗大,為了避免類被加上CLASS_ISPREVERIFIED,使用插樁,單獨(dú)放一個(gè)幫助類在獨(dú)立的dex中讓其他類調(diào)用。
dex替換
原理及實(shí)現(xiàn)
為了避免dex插樁帶來的性能損耗,dex替換采取另外的方式。原理是提供dex差量包,整體替換dex的方案。差量的方式給出patch.dex,然后將patch.dex與應(yīng)用的classes.dex合并成一個(gè)完整的dex,完整dex加載得到dexFile對象作為參數(shù)構(gòu)建一個(gè)Element對象然后整體替換掉舊的dex-Elements數(shù)組。

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



