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

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

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

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

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

下面通過Robust的原始碼來進行分析,
首先看一下打基礎包是插入的代碼邏輯,如下:
public static ChangeQuickRedirect u;
protected void onCreate(Bundle bundle) {
//為每個方法自動插入修復邏輯代碼,如果ChangeQuickRedirect為空則不執行
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的核心修復原始碼如下:
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;
}
}
優點
- 高兼容性(Robust只是在正常的使用DexClassLoader)、高穩定性,修復成功率高達99.9%
- 補丁實時生效,不需要重新啟動
- 支持方法級別的修復,包括靜態方法
- 支持增加方法和類
- 支持ProGuard的混淆、行內、優化等操作
缺點
- 代碼是侵入式的,會在原有的類中加入相關代碼
- so和資源的替換暫時不支持
- 會增大apk的體積,平均一個函式會比原來增加17.47個位元組,10萬個函式會增加1.67M
java mulitdex 原理
原理及實作
Android內部使用的是BaseDexClassLoader、PathClassLoader、DexClassLoader三個類加載器實作從DEX檔案中讀取類資料,其中PathClassLoader和DexClassLoader都是繼承自BaseDexClassLoader實作,dex檔案轉換成dexFile物件,存入Element[]陣列,findclass順序遍歷Element陣列獲取DexFile,然后執行DexFile的findclass,原始碼如下:
// 加載名字為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;
}
所以此方案的原理是Hook了ClassLoader.pathList.dexElements[],將補丁的dex插入到陣列的最前端,因為ClassLoader的findClass是通過遍歷dexElements[]中的dex來尋找類的,所以會優先查找到修復的類,從而達到修復的效果,

下面使用Nuwa的關鍵實作原始碼進行說明如下:
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;
}
優點
- 不需要考慮對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方法,如下:
@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;
}
優點
- 兼容性高
- 補丁小
- 開發透明,代碼非侵入式
缺點
- 冷啟動修復,下次啟動修復
- Dex合并記憶體消耗在vm head上,容易OOM,最后導致合并失敗
資源修復原理
Instant Run
1、構建一個新的AssetManager,并通過反射呼叫addAssertPath,把這個完整的新資源包加入到AssetManager中,這樣就得到一個含有所有新資源的AssetManager
2、找到所有值錢參考到原有AssetManager的地方,通過反射,把參考處替換為AssetManager
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);
}
}
so修復原理
介面呼叫替換
sdk提供介面替換System默認加載so庫的介面
SOPatchManger.loadLibrary(String libName)
替換
System.loadLibrary(String libName)
SOPatchManger.loadLibrary介面加載so庫的時候優先嘗試去加載sdk指定目錄下補丁的so,若不存在,則再去加載安裝apk目錄下的so庫
優點:不需要對不同sdk版本進行兼容,所以sdk版本都是System.loadLibrary這個介面
缺點:需要侵入業務代碼,替換掉System默認加載so庫的介面
反射注入
采取類似類修復反射注入方式,只要把補丁so庫的路徑插入到nativeLibraryDirectories陣列的最前面,就能夠達到加載so庫的時候是補丁so庫而不是原來so庫的目錄,從而達到修復,
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;
}
優點:不需侵入用戶介面呼叫
缺點:需要做版本兼容控制,兼容性較差
使用熱修復技術有哪些需要注意的問題?
版本管理
使用熱修復技術后由于發布流程的變化,肯定也需求采用相應的分支管理進行控制,
通常移動開發的分支管理采用特性分支,如下:
| 分支 | 描述 |
|---|---|
| master | 主分支(只能merge,不能commit,設定權限),用于管理線上版本,及時設定對應Tag |
| dev | 開發分支,每個新版本的研發根據版本號基于主分支創建,測驗通過驗證后,上線合入master分支 |
| function X | 功能分支,按需求設定,基于開發分支創建,完成功能開發后合入dev開發分支 |
接入熱修復后,推薦可參考如下分支策略:
| 分支 | 描述 |
|---|---|
| master | 主分支(只能merge,不能commit,設定權限),用于管理線上版本,及時設定對應Tag(一般3位版本號) |
| hot_fix | 熱修復分支,基于master分支創建,修復緊急問題后,測驗推送后,將hot_fix再合并到master分支,再次為master分支打tag,(一般4位版本號) |
| dev | 開發分支,每個新版本的研發根據版本號基于主分支創建,測驗通過驗證后,上線合入master分支 |
| function X | 功能分支,按需求設定,基于開發分支創建,完成功能開發后合入dev開發分支 |
注意熱修復分支的測驗及發布流程應用正常版本流程一致,保證質量,
分發監控
目前主流的熱修復方案,像Tinker及Sophix都會提供補丁的分發及監控,這也是我們選擇熱修復技術方案需要考慮的關鍵因素之一,畢竟為了保證線上版本的質量,分發控制及實時監測必不可少,
最后
想要深入了解熱修復,需要了解類加載機制,Instant Run,multidex以及java底層實作細節,JNI,AAPT和虛擬機的知識,需要龐大的知識貯備才能進行深入理解,當然Android Framwork的實作細節是非常重要的,熟悉熱修復的原理有助于我們提供自己的編程水平,提升自己解決問題的能力,最后熱修復不是簡單的客戶端SDK,它還包含了安全機制和服務端的控制邏輯,整條鏈路也不是短時間可以快速完成的,
所以為了方便朋友們更直觀快速的學習掌握Android熱修復技術,我這里收集整理一套視頻+電子書的熱修復系列學習資料,視頻教程為愛奇藝高級工程師Lance老師主講,以Qzone熱修復實戰專案為例,深入淺出的全方位講解熱修復技術,電子書是來源于阿里的《深入探索Android熱修復技術原理》對熱修復技術有很深入解讀,
由于篇幅原因,這里只做一些截圖展示,需要完整資料的朋友,可以在點贊+評論后,點擊此處免費獲取!
需要完整資料的朋友,可以在點贊+評論后,點擊此處免費獲取!
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/232126.html
標籤:其他




