相關(guān)文章
解析ClassLoader系列
前言
在Android應(yīng)用開(kāi)發(fā)中,熱修復(fù)技術(shù)被越來(lái)越多的開(kāi)發(fā)者所使用,也出現(xiàn)了很多熱修復(fù)框架,比如:AndFix、Tinker、Dexposed和Nuwa等等。如果只是會(huì)這些熱修復(fù)框架的使用那意義并不大,我們還需要了解它們的原理,這樣不管熱修復(fù)框架如何變化,只要基本原理不變,我們就可以很快的掌握它們。這一個(gè)系列不會(huì)對(duì)某些熱修復(fù)框架源碼進(jìn)行解析,而是講解熱修復(fù)框架的通用原理。
1.熱修復(fù)的產(chǎn)生概述
在開(kāi)發(fā)中我們會(huì)遇到如下的情況:
- 剛發(fā)布的版本出現(xiàn)了嚴(yán)重的bug,這就需要去解決bug、測(cè)試并打渠道包在各個(gè)應(yīng)用市場(chǎng)上重新發(fā)布,這會(huì)耗費(fèi)大量的人力物力,代價(jià)會(huì)比較大。
- 已經(jīng)改正了此前發(fā)布版本的bug,如果下一個(gè)版本是一個(gè)大版本,那么兩個(gè)版本的間隔時(shí)間會(huì)很長(zhǎng),這樣要等到下個(gè)大版本發(fā)布再修復(fù)bug,這樣此前版本的bug會(huì)長(zhǎng)期的影響用戶(hù)。
- 版本升級(jí)率不高,并且需要很長(zhǎng)時(shí)間來(lái)完成版本覆蓋,此前版本的bug就會(huì)一直影響不升級(jí)版本的用戶(hù)。
- 有一個(gè)小而重要的功能,需要短時(shí)間內(nèi)完成版本覆蓋,比如節(jié)日活動(dòng)。
為了解決上面的問(wèn)題,熱修復(fù)框架就產(chǎn)生了。對(duì)于Bug的處理,開(kāi)發(fā)人員不要過(guò)于依賴(lài)熱修復(fù)框架,在開(kāi)發(fā)的過(guò)程中還是要按照標(biāo)準(zhǔn)的流程做好自測(cè)、配合測(cè)試人員完成測(cè)試流程。
2.熱修復(fù)框架的對(duì)比
熱修復(fù)框架的種類(lèi)繁多,按照公司團(tuán)隊(duì)劃分主要有以下幾種:
| 類(lèi)別 | 成員 |
|---|---|
| 阿里系 | AndFix、Dexposed、阿里百川、Sophix |
| 騰訊系 | 微信的Tinker、QQ空間的超級(jí)補(bǔ)丁、手機(jī)QQ的QFix |
| 知名公司 | 美團(tuán)的Robust、餓了么的Amigo、美麗說(shuō)蘑菇街的Aceso |
| 其他 | RocooFix、Nuwa、AnoleFix |
雖然熱修復(fù)框架很多,但熱修復(fù)框架的核心技術(shù)主要有三類(lèi),分別是代碼修復(fù)、資源修復(fù)和動(dòng)態(tài)鏈接庫(kù)修復(fù),其中每個(gè)核心技術(shù)又有很多不同的技術(shù)方案,每個(gè)技術(shù)方案又有不同的實(shí)現(xiàn),另外這些熱修復(fù)框架仍在不斷的更新迭代中,可見(jiàn)熱修復(fù)框架的技術(shù)實(shí)現(xiàn)是繁多可變的。作為開(kāi)發(fā)需需要了解這些技術(shù)方案的基本原理,這樣就可以以不變應(yīng)萬(wàn)變。
部分熱修復(fù)框架的對(duì)比如下表所示。
| 特性 | AndFix | Tinker/Amigo | QQ空間 | Robust/Aceso |
|---|---|---|---|---|
| 即時(shí)生效 | 是 | 否 | 否 | 是 |
| 方法替換 | 是 | 是 | 是 | 是 |
| 類(lèi)替換 | 否 | 是 | 是 | 否 |
| 類(lèi)結(jié)構(gòu)修改 | 否 | 是 | 否 | 否 |
| 資源替換 | 否 | 是 | 是 | 否 |
| so替換 | 否 | 是 | 否 | 否 |
| 支持gradle | 否 | 是 | 否 | 否 |
| 支持ART | 是 | 是 | 是 | 是 |
| 支持Android7.0 | 是 | 是 | 是 | 是 |
我們可以根據(jù)上表和具體業(yè)務(wù)來(lái)選擇合適的熱修復(fù)框架,當(dāng)然上表的信息很難做到完全準(zhǔn)確,因?yàn)椴糠值臒嵝迯?fù)框架還在不斷更新迭代。
從表中也可以發(fā)現(xiàn)Tinker和Amigo擁有的特性最多,是不是就選它們呢?也不盡然,擁有的特性多也意味著框架的代碼量龐大,我們需要根據(jù)業(yè)務(wù)來(lái)選擇最合適的,假設(shè)我們只是要用到方法替換,那么使用Tinker和Amigo顯然是大材小用了。另外如果項(xiàng)目需要即時(shí)生效,那么使用Tinker和Amigo是無(wú)法滿(mǎn)足需求的。對(duì)于即時(shí)生效,AndFix、Robust和Aceso都滿(mǎn)足這一點(diǎn),這是因?yàn)锳ndFix的代碼修復(fù)采用了底層替換方案,而Robust和Aceso的代碼修復(fù)借鑒了Instant Run原理,現(xiàn)在我們就來(lái)學(xué)習(xí)代碼修復(fù)。
3.代碼修復(fù)
代碼修復(fù)主要有三個(gè)方案,分別是底層替換方案、類(lèi)加載方案和Instant Run方案。
3.1 類(lèi)加載方案
類(lèi)加載方案基于Dex分包方案,什么是Dex分包方案呢?這個(gè)得先從65536限制和LinearAlloc限制說(shuō)起。
65536限制
隨著應(yīng)用功能越來(lái)越復(fù)雜,代碼量不斷地增大,引入的庫(kù)也越來(lái)越多,可能會(huì)在編譯時(shí)提示如下異常:
com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536
這說(shuō)明應(yīng)用中引用的方法數(shù)超過(guò)了最大數(shù)65536個(gè)。產(chǎn)生這一問(wèn)題的原因就是系統(tǒng)的65536限制,65536限制的主要原因是DVM Bytecode的限制,DVM指令集的方法調(diào)用指令invoke-kind索引為16bits,最多能引用 65535個(gè)方法。
LinearAlloc限制
在安裝時(shí)可能會(huì)提示INSTALL_FAILED_DEXOPT。產(chǎn)生的原因就是LinearAlloc限制,DVM中的LinearAlloc是一個(gè)固定的緩存區(qū),當(dāng)方法數(shù)過(guò)多超出了緩存區(qū)的大小時(shí)會(huì)報(bào)錯(cuò)。
為了解決65536限制和LinearAlloc限制,從而產(chǎn)生了Dex分包方案。Dex分包方案主要做的是在打包時(shí)將應(yīng)用代碼分成多個(gè)Dex,將應(yīng)用啟動(dòng)時(shí)必須用到的類(lèi)和這些類(lèi)的直接引用類(lèi)放到主Dex中,其他代碼放到次Dex中。當(dāng)應(yīng)用啟動(dòng)時(shí)先加載主Dex,等到應(yīng)用啟動(dòng)后再動(dòng)態(tài)的加載次Dex,從而緩解了主Dex的65536限制和LinearAlloc限制。
Dex分包方案主要有兩種,分別是Google官方方案、Dex自動(dòng)拆包和動(dòng)態(tài)加載方案。因?yàn)镈ex分包方案不是本章的重點(diǎn),這里就不再過(guò)多的介紹,我們接著來(lái)學(xué)習(xí)類(lèi)加載方案。
在Android解析ClassLoader(二)Android中的ClassLoader中講到了ClassLoader的加載過(guò)程,其中一個(gè)環(huán)節(jié)就是調(diào)用DexPathList的findClass的方法,如下所示。
libcore/dalvik/src/main/java/dalvik/system/DexPathList.java
public Class<?> findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {//1
Class<?> clazz = element.findClass(name, definingContext, suppressed);//2
if (clazz != null) {
return clazz;
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
Element內(nèi)部封裝了DexFile,DexFile用于加載dex文件,因此每個(gè)dex文件對(duì)應(yīng)一個(gè)Element。
多個(gè)Element組成了有序的Element數(shù)組dexElements。當(dāng)要查找類(lèi)時(shí),會(huì)在注釋1處遍歷Element數(shù)組dexElements(相當(dāng)于遍歷dex文件數(shù)組),注釋2處調(diào)用Element的findClass方法,其方法內(nèi)部會(huì)調(diào)用DexFile的loadClassBinaryName方法查找類(lèi)。如果在Element中(dex文件)找到了該類(lèi)就返回,如果沒(méi)有找到就接著在下一個(gè)Element中進(jìn)行查找。
根據(jù)上面的查找流程,我們將有bug的類(lèi)Key.class進(jìn)行修改,再將Key.class打包成包含dex的補(bǔ)丁包Patch.jar,放在Element數(shù)組dexElements的第一個(gè)元素,這樣會(huì)首先找到Patch.dex中的Key.class去替換之前存在bug的Key.class,排在數(shù)組后面的dex文件中的存在bug的Key.class根據(jù)ClassLoader的雙親委托模式就不會(huì)被加載,這就是類(lèi)加載方案,如下圖所示。

類(lèi)加載方案需要重啟App后讓ClassLoader重新加載新的類(lèi),為什么需要重啟呢?這是因?yàn)轭?lèi)是無(wú)法被卸載的,因此要想重新加載新的類(lèi)就需要重啟App,因此采用類(lèi)加載方案的熱修復(fù)框架是不能即時(shí)生效的。
雖然很多熱修復(fù)框架采用了類(lèi)加載方案,但具體的實(shí)現(xiàn)細(xì)節(jié)和步驟還是有一些區(qū)別的,比如QQ空間的超級(jí)補(bǔ)丁和Nuwa是按照上面說(shuō)得將補(bǔ)丁包放在Element數(shù)組的第一個(gè)元素得到優(yōu)先加載。微信Tinker將新舊apk做了diff,得到patch.dex,然后將patch.dex與手機(jī)中apk的classes.dex做合并,生成新的classes.dex,然后在運(yùn)行時(shí)通過(guò)反射將classes.dex放在Element數(shù)組的第一個(gè)元素。餓了么的Amigo則是將補(bǔ)丁包中每個(gè)dex 對(duì)應(yīng)的Element取出來(lái),之后組成新的Element數(shù)組,在運(yùn)行時(shí)通過(guò)反射用新的Element數(shù)組替換掉現(xiàn)有的Element 數(shù)組。
采用類(lèi)加載方案的主要是以騰訊系為主,包括微信的Tinker、QQ空間的超級(jí)補(bǔ)丁、手機(jī)QQ的QFix、餓了么的Amigo和Nuwa等等。
3.2 底層替換方案
與類(lèi)加載方案不同的是,底層替換方案不會(huì)再次加載新類(lèi),而是直接在Native層修改原有類(lèi),由于是在原有類(lèi)進(jìn)行修改限制會(huì)比較多,不能夠增減原有類(lèi)的方法和字段,如果我們?cè)黾恿朔椒〝?shù),那么方法索引數(shù)也會(huì)增加,這樣訪(fǎng)問(wèn)方法時(shí)會(huì)無(wú)法通過(guò)索引找到正確的方法,同樣的字段也是類(lèi)似的情況。
底層替換方案和反射的原理有些關(guān)聯(lián),就拿方法替換來(lái)說(shuō),方法反射我們可以調(diào)用java.lang.Class.getDeclaredMethod,假設(shè)我們要反射Key的show方法,會(huì)調(diào)用如下所示。
Key.class.getDeclaredMethod("show").invoke(Key.class.newInstance());
Android 8.0的invoke方法,如下所示。
libcore/ojluni/src/main/java/java/lang/reflect/Method.java
@FastNative
public native Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException, InvocationTargetException;
invoke方法是個(gè)native方法,對(duì)應(yīng)Jni層的代碼為:
art/runtime/native/java_lang_reflect_Method.cc
static jobject Method_invoke(JNIEnv* env, jobject javaMethod, jobject javaReceiver,
jobject javaArgs) {
ScopedFastNativeObjectAccess soa(env);
return InvokeMethod(soa, javaMethod, javaReceiver, javaArgs);
Method_invoke函數(shù)中又調(diào)用了InvokeMethod函數(shù):
art/runtime/reflection.cc
jobject InvokeMethod(const ScopedObjectAccessAlreadyRunnable& soa, jobject javaMethod,
jobject javaReceiver, jobject javaArgs, size_t num_frames) {
...
ObjPtr<mirror::Executable> executable = soa.Decode<mirror::Executable>(javaMethod);
const bool accessible = executable->IsAccessible();
ArtMethod* m = executable->GetArtMethod();//1
...
}
注釋1處獲取傳入的javaMethod(Key的show方法)在ART虛擬機(jī)中對(duì)應(yīng)的一個(gè)ArtMethod指針,ArtMethod結(jié)構(gòu)體中包含了Java方法的所有信息,包括執(zhí)行入口、訪(fǎng)問(wèn)權(quán)限、所屬類(lèi)和代碼執(zhí)行地址等等,ArtMethod結(jié)構(gòu)如下所示。
art/runtime/art_method.h
class ArtMethod FINAL {
...
protected:
GcRoot<mirror::Class> declaring_class_;
std::atomic<std::uint32_t> access_flags_;
uint32_t dex_code_item_offset_;
uint32_t dex_method_index_;
uint16_t method_index_;
uint16_t hotness_count_;
struct PtrSizedFields {
ArtMethod** dex_cache_resolved_methods_;//1
void* data_;
void* entry_point_from_quick_compiled_code_;//2
} ptr_sized_fields_;
}
ArtMethod結(jié)構(gòu)中比較重要的字段是注釋1處的dex_cache_resolved_methods_和注釋2處的entry_point_from_quick_compiled_code_,它們是方法的執(zhí)行入口,當(dāng)我們調(diào)用某一個(gè)方法時(shí)(比如Key的show方法),就會(huì)取得show方法的執(zhí)行入口,通過(guò)執(zhí)行入口就可以跳過(guò)去執(zhí)行show方法。
替換ArtMethod結(jié)構(gòu)體中的字段或者替換整個(gè)ArtMethod結(jié)構(gòu)體,這就是底層替換方案。
AndFix采用的是替換ArtMethod結(jié)構(gòu)體中的字段,這樣會(huì)有兼容問(wèn)題,因?yàn)閺S(chǎng)商可能會(huì)修改ArtMethod結(jié)構(gòu)體,導(dǎo)致方法替換失敗。Sophix采用的是替換整個(gè)ArtMethod結(jié)構(gòu)體,這樣不會(huì)存在兼容問(wèn)題。
底層替換方案直接替換了方法,可以立即生效不需要重啟。采用底層替換方案主要是阿里系為主,包括AndFix、Dexposed、阿里百川、Sophix。
3.3 Instant Run方案
除了資源修復(fù),代碼修復(fù)同樣也可以借鑒Instant Run的原理, 可以說(shuō)Instant Run的出現(xiàn)推動(dòng)了熱修復(fù)框架的發(fā)展。
Instant Run在第一次構(gòu)建apk時(shí),使用ASM在每一個(gè)方法中注入了類(lèi)似如下的代碼:
IncrementalChange localIncrementalChange = $change;//1
if (localIncrementalChange != null) {//2
localIncrementalChange.access$dispatch(
"onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
paramBundle });
return;
}
其中注釋1處是一個(gè)成員變量localIncrementalChange ,它的值為$change,$change實(shí)現(xiàn)了IncrementalChange這個(gè)抽象接口。當(dāng)我們點(diǎn)擊InstantRun時(shí),如果方法沒(méi)有變化則$change為null,就調(diào)用return,不做任何處理。如果方法有變化,就生成替換類(lèi),這里我們假設(shè)MainActivity的onCreate方法做了修改,就會(huì)生成替換類(lèi)MainActivity$override,這個(gè)類(lèi)實(shí)現(xiàn)了IncrementalChange接口,同時(shí)也會(huì)生成一個(gè)AppPatchesLoaderImpl類(lèi),這個(gè)類(lèi)的getPatchedClasses方法會(huì)返回被修改的類(lèi)的列表(里面包含了MainActivity),根據(jù)列表會(huì)將MainActivity的$change設(shè)置為MainActivity$override,因此滿(mǎn)足了注釋2的條件,會(huì)執(zhí)行MainActivity$override的access$dispatch方法,access$dispatch方法中會(huì)根據(jù)參數(shù)"onCreate.(Landroid/os/Bundle;)V"執(zhí)行MainActivity$override的onCreate方法,從而實(shí)現(xiàn)了onCreate方法的修改。
借鑒Instant Run的原理的熱修復(fù)框架有Robust和Aceso。