前言
熱修復(fù)
到現(xiàn)在2022年已經(jīng)不是一個(gè)新名詞,但是作為Android開發(fā)核心技術(shù)棧的一部分太抓,我這里還得來一次冷飯熱炒空闲。
隨著移動(dòng)端業(yè)務(wù)復(fù)雜程度的增加,傳統(tǒng)的版本更新流程顯然無法滿足業(yè)務(wù)和開發(fā)者的需求走敌,
熱修復(fù)技術(shù)的推出在很大程度上改善了這一局面进副。國(guó)內(nèi)大部分成熟的主流 App都擁有自己的熱更新技術(shù),像手淘悔常、支付寶、微信给赞、QQ机打、餓了么、美團(tuán)等片迅。
可以說残邀,一個(gè)好的熱修復(fù)技術(shù),將為你的 App助力百倍柑蛇。對(duì)于每一個(gè)想在 Android 開發(fā)領(lǐng)域有所造詣的開發(fā)者芥挣,掌握熱修復(fù)技術(shù)更是必備的素質(zhì)。
熱修復(fù)
是 Android 大廠面試中高頻面試知識(shí)點(diǎn)耻台,也是我們必須要掌握的知識(shí)點(diǎn)空免。熱修復(fù)技術(shù),可以看作 Android平臺(tái)發(fā)展成熟至一定階段的必然產(chǎn)物盆耽。
Android熱修復(fù)了解嗎蹋砚?修復(fù)哪些東西?
常見熱修復(fù)框架對(duì)比以及各原理分析摄杂?
1.什么是熱修復(fù)
熱修復(fù)說白了就是不再使用傳統(tǒng)的應(yīng)用商店更新或者自更新方式坝咐,使用補(bǔ)丁包推送的方式在用戶無感知的情況下,修復(fù)應(yīng)用bug或者推送新的需求
傳統(tǒng)更新
和熱更新
過程對(duì)比如下:
熱修復(fù)優(yōu)缺點(diǎn)
:
-
優(yōu)點(diǎn):
- 1.只需要打補(bǔ)丁包析恢,不需要重新發(fā)版本墨坚。
- 2.用戶無感知,不需要重新下載最新應(yīng)用
- 3.修復(fù)成功率高
-
缺點(diǎn):
- 補(bǔ)丁包濫用映挂,容易導(dǎo)致應(yīng)用版本不可控泽篮,需要開發(fā)一套完整的補(bǔ)丁包更新機(jī)制,會(huì)增加一定的成本
2.熱修復(fù)方案
首先我們得知道熱修復(fù)修復(fù)哪些東西袖肥?
- 1.代碼修復(fù)
- 2.資源修復(fù)
- 3.動(dòng)態(tài)庫修復(fù)
2.1:代碼修復(fù)方案
從技術(shù)角度來說咪辱,我們的目的是非常明確的:把錯(cuò)誤的代碼替換成正確的代碼。
注意這里的替換椎组,并不是直接擦寫dx文件油狂,而是提供一份新的正確代碼,讓應(yīng)用運(yùn)行時(shí)繞過錯(cuò)誤代碼,執(zhí)行新的正確代碼专筷。
想法簡(jiǎn)單直接弱贼,但實(shí)現(xiàn)起來并不容易。目前主要有三類技術(shù)方案:
2.1.1.類加載方案
之前分析類加載機(jī)制有說過:
加載流程先是遵循雙親委派原則磷蛹,如果委派原則沒有找到此前加載過此類吮旅,
則會(huì)調(diào)用CLassLoader的findClass方法,再去BaseDexClassLoader下面的dexElements數(shù)組中查找味咳,如果沒有找到庇勃,最終調(diào)用defineClassNative方法加載
代碼修復(fù)就是基于這點(diǎn):
將新的做了修復(fù)的dex文件,通過反射注入到BaseDexClassLoader的dexElements數(shù)組的第一個(gè)位置上dexElements[0]槽驶,下次重新啟動(dòng)應(yīng)用加載類的時(shí)候责嚷,會(huì)優(yōu)先加載做了修復(fù)的dex文件,這樣就達(dá)到了修復(fù)代碼的目的掂铐。原理很簡(jiǎn)單
代碼如下:
public class Hotfix {
public static void patch(Context context, String patchDexFile, String patchClassName)
throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
//獲取系統(tǒng)PathClassLoader的"dexElements"屬性值
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
Object origDexElements = getDexElements(pathClassLoader);
//新建DexClassLoader并獲取“dexElements”屬性值
String otpDir = context.getDir("dex", 0).getAbsolutePath();
Log.i("hotfix", "otpdir=" + otpDir);
DexClassLoader nDexClassLoader = new DexClassLoader(patchDexFile, otpDir, patchDexFile, context.getClassLoader());
Object patchDexElements = getDexElements(nDexClassLoader);
//將patchDexElements插入原origDexElements前面
Object allDexElements = combineArray(origDexElements, patchDexElements);
//將新的allDexElements重新設(shè)置回pathClassLoader
setDexElements(pathClassLoader, allDexElements);
//重新加載類
pathClassLoader.loadClass(patchClassName);
}
private static Object getDexElements(ClassLoader classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
//首先獲取ClassLoader的“pathList”實(shí)例
Field pathListField = Class.forName("dalvik.system.BaseDexClassLoader").getDeclaredField("pathList");
pathListField.setAccessible(true);//設(shè)置為可訪問
Object pathList = pathListField.get(classLoader);
//然后獲取“pathList”實(shí)例的“dexElements”屬性
Field dexElementField = pathList.getClass().getDeclaredField("dexElements");
dexElementField.setAccessible(true);
//讀取"dexElements"的值
Object elements = dexElementField.get(pathList);
return elements;
}
//合拼dexElements
private static Object combineArray(Object obj, Object obj2) {
Class componentType = obj2.getClass().getComponentType();
//讀取obj長(zhǎng)度
int length = Array.getLength(obj);
//讀取obj2長(zhǎng)度
int length2 = Array.getLength(obj2);
Log.i("hotfix", "length=" + length + ",length2=" + length2);
//創(chuàng)建一個(gè)新Array實(shí)例罕拂,長(zhǎng)度為ojb和obj2之和
Object newInstance = Array.newInstance(componentType, length + length2);
for (int i = 0; i < length + length2; i++) {
//把obj2元素插入前面
if (i < length2) {
Array.set(newInstance, i, Array.get(obj2, i));
} else {
//把obj元素依次放在后面
Array.set(newInstance, i, Array.get(obj, i - length2));
}
}
//返回新的Array實(shí)例
return newInstance;
}
private static void setDexElements(ClassLoader classLoader, Object dexElements) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
//首先獲取ClassLoader的“pathList”實(shí)例
Field pathListField = Class.forName("dalvik.system.BaseDexClassLoader").getDeclaredField("pathList");
pathListField.setAccessible(true);//設(shè)置為可訪問
Object pathList = pathListField.get(classLoader);
//然后獲取“pathList”實(shí)例的“dexElements”屬性
Field declaredField = pathList.getClass().getDeclaredField("dexElements");
declaredField.setAccessible(true);
//設(shè)置"dexElements"的值
declaredField.set(pathList, dexElements);
}
}
類加載過程如下:
微信Tinker
,QQ 空間的超級(jí)補(bǔ)丁
全陨、手 QQ 的QFix
爆班、餓了 么的 Amigo
和 Nuwa
等都是使用這個(gè)方式
缺點(diǎn):因?yàn)轭惣虞d后無法卸載,所以類加載方案必須重啟App辱姨,讓bug類重新加載后才能生效柿菩。
2.1.2:底層替換方案
底層替換方案不會(huì)再次加載新類,而是直接在 Native 層 修改原有類雨涛,
這里我們需要提到Art虛擬機(jī)中ArtMethod
:
每一個(gè)Java方法在Art虛擬機(jī)中都對(duì)應(yīng)著一個(gè)ArtMethod
碗旅,ArtMethod記錄了這個(gè)Java方法的所有信息,包括所屬類镜悉、訪問權(quán)限祟辟、代碼執(zhí)行地址等。
結(jié)構(gòu)如下:
// art/runtime/art_method.h
class ArtMethod FINAL {
...
protected:
GcRoot<mirror::Class> declaring_class_;
GcRoot<mirror::PointerArray> dex_cache_resolved_methods_;
GcRoot<mirror::ObjectArray<mirror::Class>> dex_cache_resolved_types_;
uint32_t access_flags_;
uint32_t dex_code_item_offset_;
uint32_t dex_method_index_;
uint32_t method_index_;
struct PACKED(4) PtrSizedFields {
void* entry_point_from_interpreter_; // 1
void* entry_point_from_jni_;
void* entry_point_from_quick_compiled_code_; //2
} ptr_sized_fields_;
...
}
在 ArtMethod結(jié)構(gòu)體中侣肄,最重要的就是 注釋1和注釋2標(biāo)注的內(nèi)容旧困,從名字可以看出來,他們就是方法的執(zhí)行入口稼锅。
我們知道吼具,Java代碼在Android中會(huì)被編譯為 Dex Code。
Art虛擬機(jī)中可以采用解釋模式或者 AOT機(jī)器碼模式執(zhí)行 Dex Code
-
解釋模式:
就是去除Dex Code矩距,逐條解釋執(zhí)行拗盒。
如果方法的調(diào)用者是以解釋模式運(yùn)行的,在調(diào)用這個(gè)方法時(shí)锥债,就會(huì)獲取這個(gè)方法的 entry_point_from_interpreter_陡蝇,然后跳轉(zhuǎn)執(zhí)行痊臭。 -
AOT模式:
就會(huì)預(yù)先編譯好 Dex Code對(duì)應(yīng)的機(jī)器碼,然后在運(yùn)行期直接執(zhí)行機(jī)器碼登夫,不需要逐條解釋執(zhí)行Dex Code广匙。
如果方法的調(diào)用者是以AOT機(jī)器碼方式執(zhí)行的,在調(diào)用這個(gè)方法時(shí)恼策,就是跳轉(zhuǎn)到 entry_point_from_quick_compiled_code_中執(zhí)行鸦致。
那是不是只需要替換這個(gè)幾個(gè) entry_point_* 入口地址就能夠?qū)崿F(xiàn)方法替換了呢?
并沒有那么簡(jiǎn)單涣楷,因?yàn)椴徽撌墙忉屇J竭€是AOT模式分唾,在運(yùn)行期間還會(huì)需要調(diào)用ArtMethod中的其他成員字段
AndFix采用的是改變指針指向:
// AndFix/jni/art/art_method_replace_6_0.cpp
void replace_6_0(JNIEnv* env, jobject src, jobject dest) {
art::mirror::ArtMethod* smeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(src); // 1
art::mirror::ArtMethod* dmeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(dest); // 2
...
// 3
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_);
}
缺點(diǎn):存在一些兼容性問題,由于ArtMethod結(jié)構(gòu)體是Android開源的一部分狮斗,所以每個(gè)手機(jī)廠商都可能會(huì)去更改這部分的內(nèi)容鳍寂,這就可能導(dǎo)致ArtMethod替換方案在某些機(jī)型上面出現(xiàn)未知錯(cuò)誤。
Sophix為了規(guī)避上面的AndFix的風(fēng)險(xiǎn)情龄,采用直接替換整個(gè)結(jié)構(gòu)體
。這樣不管手機(jī)廠商如何更改系統(tǒng)捍壤,我們都可以正確定位到方法地址
2.4.3:install run
方案
Instant Run 方案的核心思想是——插樁骤视,在編譯時(shí)通過插樁在每一個(gè)方法中插入代碼,修改代碼邏輯鹃觉,在需要時(shí)繞過錯(cuò)誤方法专酗,調(diào)用patch類的正確方法。
首先盗扇,在編譯時(shí)Instant Run為每個(gè)類插入IncrementalChange變量
IncrementalChange $change;
為每一個(gè)方法添加類似如下代碼:
public void onCreate(Bundle savedInstanceState) {
IncrementalChange var2 = $change;
//$change不為null祷肯,表示該類有修改,需要重定向
if(var2 != null) {
//通過access$dispatch方法跳轉(zhuǎn)到patch類的正確方法
var2.access$dispatch("onCreate.(Landroid/os/Bundle;)V", new Object[]{this, savedInstanceState});
} else {
super.onCreate(savedInstanceState);
this.setContentView(2130968601);
this.tv = (TextView)this.findViewById(2131492944);
}
}
如上代碼疗隶,當(dāng)一個(gè)類被修改后佑笋,Instant Run會(huì)為這個(gè)類新建一個(gè)類,命名為xxx&override斑鼻,且實(shí)現(xiàn)IncrementalChange接口蒋纬,并且賦值給原類的$change變量。
public class MainActivity$override implements IncrementalChange {
}
此時(shí)坚弱,在運(yùn)行時(shí)原類中每個(gè)方法的var2 != null蜀备,通過accessdispatch(參數(shù)是方法名和原參數(shù))定位到patch類MainActivitydispatch(參數(shù)是方法名和原參數(shù))定位到patch類MainActivityoverride中修改后的方法。
Instant Run是google在AS2.0時(shí)用來實(shí)現(xiàn)“熱部署”的荒叶,同時(shí)也為“熱修復(fù)”提供了一個(gè)絕佳的思路碾阁。美團(tuán)的Robust就是基于此。
2.2:資源修復(fù)方案
這里我們來看看install run的原理即可些楣,市面上的常見修復(fù)方案大部分都是基于此方法脂凶。
public static void monkeyPatchExistingResources(Context context,
String externalResourceFile, Collection<Activity> activities) {
if (externalResourceFile == null) {
return;
}
try {
// 創(chuàng)建一個(gè)新的AssetManager
AssetManager newAssetManager = (AssetManager) AssetManager.class
.getConstructor(new Class[0]).newInstance(new Object[0]); // ... 1
Method mAddAssetPath = AssetManager.class.getDeclaredMethod(
"addAssetPath", new Class[] { String.class }); // ... 2
mAddAssetPath.setAccessible(true);
// 通過反射調(diào)用addAssetPath方法加載外部的資源(SD卡資源)
if (((Integer) mAddAssetPath.invoke(newAssetManager,
new Object[] { externalResourceFile })).intValue() == 0) { // ... 3
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]);
if (activities != null) {
for (Activity activity : activities) {
Resources resources = activity.getResources(); // ... 4
try {
// 反射得到Resources的AssetManager類型的mAssets字段
Field mAssets = Resources.class
.getDeclaredField("mAssets"); // ... 5
mAssets.setAccessible(true);
// 將mAssets字段的引用替換為新創(chuàng)建的newAssetManager
mAssets.set(resources, newAssetManager); // ... 6
} catch (Throwable ignore) {
...
}
// 得到Activity的Resources.Theme
Resources.Theme theme = activity.getTheme();
try {
try {
// 反射得到Resources.Theme的mAssets字段
Field ma = Resources.Theme.class
.getDeclaredField("mAssets");
ma.setAccessible(true);
// 將Resources.Theme的mAssets字段的引用替換為新創(chuàng)建的newAssetManager
ma.set(theme, newAssetManager); // ... 7
} catch (NoSuchFieldException ignore) {
...
}
...
} catch (Throwable e) {
Log.e("InstantRun",
"Failed to update existing theme for activity "
+ activity, e);
}
pruneResourceCaches(resources);
}
}
/**
* 根據(jù)SDK版本的不同宪睹,用不同的方式得到Resources 的弱引用集合
*/
Collection<WeakReference<Resources>> 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<?, WeakReference<Resources>> 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<?, WeakReference<Resources>> map = (HashMap) fMActiveResources
.get(thread);
references = map.values();
}
//遍歷并得到弱引用集合中的 Resources ,將 Resources mAssets 字段引用替換成新的 AssetManager
for (WeakReference<Resources> 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) {
...
}
resources.updateConfiguration(resources.getConfiguration(),
resources.getDisplayMetrics());
}
}
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}
- 在注釋1處創(chuàng)建一個(gè)新的 AssetManager 艰猬,
- 在注釋2 和注釋3 處通過反射調(diào)用 addAssetPath 方法加載外部( SD 卡)的資源横堡。
- 在注釋4 處遍歷 Activity 列表,得到每個(gè) Activity 的 Resources 冠桃,
- 在注釋5 處通過反射得到 Resources 的 AssetManager 類型的 rnAssets 字段 命贴,
- 注釋6處改寫 mAssets 字段的引用為新的 AssetManager 。
采用同樣的方式食听,
- 在注釋7處將 Resources. Theme 的 m Assets 字段 的引用替換為新創(chuàng)建的 AssetManager 胸蛛。
- 緊接著 根據(jù) SDK 版本的不同,用不同的方式得到 Resources 的弱引用集合樱报,
- 再遍歷這個(gè)弱引用集合葬项, 將弱引用集合中的 Resources 的 mAssets 字段引用都替換成新創(chuàng)建的 AssetManager 。
資源修復(fù)原理
:
- 1.創(chuàng)建新的AssetManager迹蛤,通過反射調(diào)用addAssetPath方法民珍,加載外部資源,這樣新創(chuàng)建的AssetManager就含有了外部資源
- 2.將AssetManager類型的mAsset字段全部用新創(chuàng)建的AssetManager對(duì)象替換盗飒。這樣下次加載資源文件的時(shí)候就可以找到包含外部資源文件的AssetManager嚷量。
2.3:動(dòng)態(tài)鏈接庫so的修復(fù)
1.接口調(diào)用替換方案:
sdk提供接口替換System默認(rèn)加載so庫接口
SOPatchManager.loadLibrary(String libName) -> System.loadLibrary(String libName)
SOPatchManager.loadLibrary接口加載 so庫的時(shí)候優(yōu)先嘗試去加載sdk 指定目錄下的補(bǔ)丁so,
加載策略
如下:
如果存在則加載補(bǔ)丁 so庫而不會(huì)去加載安裝apk安裝目錄下的so庫
如果不存在補(bǔ)丁so逆趣,那么調(diào)用System.loadLibrary去加載安裝apk目錄下的 so庫蝶溶。
我們可以很清楚的看到這個(gè)方案的優(yōu)缺點(diǎn):
優(yōu)點(diǎn):不需要對(duì)不同 sdk 版本進(jìn)行兼容,因?yàn)樗械?sdk 版本都有 System.loadLibrary 這個(gè)接口宣渗。
缺點(diǎn):調(diào)用方需要替換掉 System 默認(rèn)加載 so 庫接口為 sdk提供的接口抖所, 如果是已經(jīng)編譯混淆好的三方庫的so 庫需要 patch,那么是很難做到接口的替換
雖然這種方案實(shí)現(xiàn)簡(jiǎn)單痕囱,同時(shí)不需要對(duì)不同 sdk版本區(qū)分處理田轧,但是有一定的局限性沒法修復(fù)三方包的so庫同時(shí)需要強(qiáng)制侵入接入方接口調(diào)用,接著我們來看下反射注入方案鞍恢。
2涯鲁、反射注入方案
前面介紹過 System. loadLibrary ( "native-lib"); 加載 so庫的原理,其實(shí)native-lib 這個(gè) so 庫最終傳給 native 方法執(zhí)行的參數(shù)是 so庫在磁盤中的完整路徑有序,比如:/data/app-lib/com.taobao.jni-2/libnative-lib.so, so庫會(huì)在 DexPathList.nativeLibraryDirectories/nativeLibraryPathElements 變量所表示的目錄下去遍歷搜索
sdk<23 DexPathList.findLibrary 實(shí)現(xiàn)如下
可以發(fā)現(xiàn)會(huì)遍歷 nativeLibraryDirectories數(shù)組抹腿,如果找到了 loUtils.canOpenReadOnly (path)返回為 true, 那么就直接返回該 path, loUtils.canOpenReadOnly (path)返回為 true 的前提肯定是需要 path 表示的 so文件存 在的。那么我們可以采取類似類修復(fù)反射注入方式旭寿,只要把我們的補(bǔ)丁so庫的路徑插入到nativeLibraryDirectories數(shù)組的最前面就能夠達(dá)到加載so庫的時(shí)候是補(bǔ)丁 庫而不是原來so庫的目錄警绩,從而達(dá)到修復(fù)的目的。
sdk>=23 DexPathList.findLibrary 實(shí)現(xiàn)如下
sdk23 以上 findLibrary 實(shí)現(xiàn)已經(jīng)發(fā)生了變化盅称,如上所示肩祥,那么我們只需要把補(bǔ)丁so庫的完整路徑作為參數(shù)構(gòu)建一個(gè)Element對(duì)象后室,然后再插入到nativeLibraryPathElements 數(shù)組的最前面就好了。
- 優(yōu)點(diǎn):可以修復(fù)三方庫的so庫混狠。同時(shí)接入方不需要像方案1 —樣強(qiáng)制侵入用 戶接口調(diào)用
- 缺點(diǎn):需要不斷的對(duì) sdk 進(jìn)行適配岸霹,如上 sdk23 為分界線,findLibrary接口實(shí)現(xiàn)已經(jīng)發(fā)生了變化将饺。
對(duì)于 so庫的修復(fù)方案目前更多采取的是接口調(diào)用替換方式贡避,需要強(qiáng)制侵入用戶 接口調(diào)用。
目前我們的so文件修復(fù)方案采取的是反射注入的方案予弧,重啟生效刮吧。具有更好的普遍性丑搔。
如果有so文件修復(fù)實(shí)時(shí)生效的需求酗昼,也是可以做到的,只是有些限制情況颠通。
常見熱修復(fù)框架蚓庭?
特性 | Dexposed | AndFix | Tinker/Amigo | QQ Zone | Robust/Aceso | Sophix |
---|---|---|---|---|---|---|
技術(shù)原理 | native底層替換 | native底層替換 | 類加載 | 類加載 | Instant Run | 混合 |
所屬 | 阿里 | 阿里 | 微信/餓了么 | QQ空間 | 美團(tuán)/蘑菇街 | 阿里 |
即時(shí)生效 | YES | YES | NO | NO | YES | 混合 |
方法替換 | YES | YES | YES | YES | YES | YES |
類替換 | NO | NO | YES | YES | YES | YES |
類結(jié)構(gòu)修改 | NO | NO | YES | NO | NO | YES |
資源替換 | NO | NO | YES | YES | NO | YES |
so替換 | NO | NO | YES | NO | NO | YES |
支持gradle | NO | NO | YES | YES | YES | YES |
支持ART | NO | YES | YES | YES | YES | YES |
可以看出致讥,阿里系多采用native底層方案,騰訊系多采用類加載機(jī)制器赞。其中垢袱,Sophix是商業(yè)化方案;Tinker/Amigo支持特性較多拳魁,同時(shí)也更復(fù)雜,如果需要修復(fù)資源和so撮弧,可以選擇潘懊;如果僅需要方法替換,且需要即時(shí)生效贿衍,Robust是不錯(cuò)的選擇授舟。
總結(jié):
盡管熱修復(fù)(或熱更新)相對(duì)于迭代更新有諸多優(yōu)勢(shì),市面上也有很多開源方案可供選擇贸辈,但目前熱修復(fù)依然無法替代迭代更新模式释树。有如下原因:
熱修復(fù)框架多多少少會(huì)增加性能開銷,或增加APK大小
熱修復(fù)技術(shù)本身存在局限擎淤,比如有些方案無法替換so或資源文件
熱修復(fù)方案的兼容性奢啥,有些方案無法同時(shí)兼顧Dalvik和ART,有些深度定制系統(tǒng)也無法正常工作
監(jiān)管風(fēng)險(xiǎn)嘴拢,比如蘋果系統(tǒng)嚴(yán)格限制熱修復(fù)
所以桩盲,對(duì)于功能迭代和常規(guī)bug修復(fù),版本迭代更新依然是主流席吴。一般的代碼修復(fù)赌结,使用Robust可以解決捞蛋,如果還需要修復(fù)資源或so庫,可以考慮Tinker柬姚。
參考文章
掃描下方的微信二維碼拟杉,這里有一套完整的移動(dòng)端開發(fā)知識(shí)體系,助你進(jìn)階高級(jí)開發(fā)量承。