Android熱修復技術,你會怎麼選?
前言
目前Android業內,熱修復技術百花齊放,各大廠都推出了自己的熱修復方案,使用的技術方案也各有所異,當然各個方案也都存在各自的侷限性。在面對眾多的方案,希望通過梳理這些熱修復方案的對比及實現原理,掌握熱修復技術的本質,同時也對專案接入做好準備。
什麼是熱修復技術?
關於熱修復這個名詞,並不陌生。相信大家都有過更新window補丁的經歷,通過補丁可以動態修復系統的漏洞,只不過這個過程對使用者而言是可選及自行操作。
那麼關於Android平臺的熱修復技術,簡單來說,就是通過下發補丁包,讓已安裝的客戶端動態更新,讓使用者可以不用重新安裝APP,就能夠修復軟體缺陷的一種技術。

image
隨著熱修復技術的發展,不僅可以修復程式碼,同時可以修復資原始檔及SO庫。

image
為什麼要使用熱修復技術?
在回答這個問題之前,我覺得應該先思考如下幾個問題。
- 開發上線的版本能保證不存在Bug麼?
- 修復後的版本能保證使用者都及時更新麼?
- 如何最大化減少線上Bug對業務的影響?
從這些角度來說,相信大家應該都能有所體會,熱修復技術帶來的優勢不言而喻。
- 可快速修復,避免線上Bug帶來的業務損失,把損失降到最低。
- 保證客戶端的更新率,無須使用者進行版本升級安裝
- 良好的使用者體驗,無感知修復異常。節省使用者下載安裝成本。
怎麼選擇熱修復技術方案?
國內主流的技術方案
1、阿里系
名稱 | 說明 |
---|---|
ofollow,noindex">AndFix | 開源,實時生效 |
HotFix | 阿里百川,未開源,免費、實時生效 |
Sophix | 未開源,商業收費,實時生效/冷啟動修復 |
HotFix是AndFix的優化版本,Sophix是HotFix的優化版本。目前阿里系主推是Sophix。
2、騰訊系
名稱 | 說明 |
---|---|
Qzone超級補丁 | QQ空間,未開源,冷啟動修復 |
QFix | 手Q團隊,開源,冷啟動修復 |
Tinker | 微信團隊,開源,冷啟動修復。提供分發管理, 基礎版免費 |
3、其他
名稱 | 說明 |
---|---|
Robust | 美團, 開源,實時修復 |
Nuwa | 大眾點評,開源,冷啟動修復 |
Amigo | 餓了麼,開源,冷啟動修復 |
方案對比
方案對比 | Sophix | Tinker | nuwa | AndFix | Robust | Amigo |
---|---|---|---|---|---|---|
類替換 | yes | yes | yes | no | no | yes |
So替換 | yes | yes | no | no | no | yes |
資源替換 | yes | yes | yes | no | no | yes |
全平臺支援 | yes | yes | yes | no | yes | yes |
即時生效 | 同時支援 | no | no | yes | yes | no |
效能損耗 | 較少 | 較小 | 較大 | 較小 | 較小 | 較小 |
補丁包大小 | 小 | 較小 | 較大 | 一般 | 一般 | 較大 |
開發透明 | yes | yes | yes | no | no | yes |
複雜度 | 傻瓜式接入 | 複雜 | 較低 | 複雜 | 複雜 | 較低 |
Rom體積 | 較小 | Dalvik較大 | 較小 | 較小 | 較小 | 大 |
成功率 | 高 | 較高 | 較高 | 一般 | 最高 | 較高 |
熱度 | 高 | 高 | 低 | 低 | 高 | 低 |
開源 | no | yes | yes | yes | yes | yes |
收費 | 收費(設有免費閾值) | 收費(基礎版免費,但有限制) | 免費 | 免費 | 免費 | 免費 |
監控 | 提供分發控制及監控 | 提供分發控制及監控 | no | no | no | no |
參考Tinker及Sophix官方對比
怎麼選?
怎麼選?這個只能說一切看需求。如果公司綜合實力強,完全考慮自研都沒問題,但需要綜合考慮成本及維護。下面給出2點建議,如下:
1、專案需求
- 只需要簡單的方法級別Bug修復?
- 需要資源及so庫的修復?
- 對平臺相容性要求及成功率要求?
- 有需求對分發進行控制,對監控資料進行統計,補丁包進行管理?
- 公司資源是否支援商業付費?
2、學習及使用成本
- 整合難度
- 程式碼侵入性
- 除錯維護
3、選擇大廠
- 技術性能有保障
- 有專人維護
- 熱度高,開源社群活躍
如果考慮付費,推薦選擇阿里的Sophix,Sophix是綜合優化的產物,功能完善、開發簡單透明、提供分發及監控管理。
如果不考慮付費,只需支援方法級別的Bug修復,不支援資源及so,推薦使用Robust。
如果考慮需要同時支援資源及so,推薦使用Tinker。
最後如果公司綜合實力強,可考慮自研,靈活性及可控制最強。
從Github上的熱度及提交記錄上看,nuwa、AndFix、Amigo等的提交都是2 years ago。
內業主要熱修復技術方案原理?
技術分類

image
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 官方介紹示例圖
下面通過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來尋找類的。所以會優先查詢到修復的類。從而達到修復的效果。

圖片引用自QQ空間熱修復介紹
下面使用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採用的方案,並且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); ArrayMaparrayMap = (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); HashMapmap = (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都會提供補丁的分發及監控。這也是我們選擇熱修復技術方案需要考慮的關鍵因素之一。畢竟為了保證線上版本的質量,分發控制及實時監測必不可少。
總結
Android熱修復技術發展至今已經是百花齊放,各大廠也都推出了自己的技術框架。也有像阿里推出的《深入探索Android熱修復技術原理》對熱修復技術的深入解讀。本文大部分總結也都參考這本經典。鑑於熱修復技術的多種多樣,所以才決定進行梳理,提供選擇時的一些注意事項及參考建議,也加深自己對熱修復技術的理解。總的來說,還是收穫滿滿。
參考資料
《深入探索Android熱修復技術原理》