Android熱修復原理(一)熱修復框架對比和代碼修復

相關文章
解析ClassLoader系列

前言

在Android應用開發(fā)中剩蟀,熱修復技術被越來越多的開發(fā)者所使用盼产,也出現(xiàn)了很多熱修復框架瓤逼,比如:AndFix踢关、Tinker伞鲫、Dexposed和Nuwa等等。如果只是會這些熱修復框架的使用那意義并不大签舞,我們還需要了解它們的原理秕脓,這樣不管熱修復框架如何變化,只要基本原理不變儒搭,我們就可以很快的掌握它們吠架。這一個系列不會對某些熱修復框架源碼進行解析,而是講解熱修復框架的通用原理搂鲫。

1.熱修復的產(chǎn)生概述

在開發(fā)中我們會遇到如下的情況:

  1. 剛發(fā)布的版本出現(xiàn)了嚴重的bug傍药,這就需要去解決bug、測試并打渠道包在各個應用市場上重新發(fā)布默穴,這會耗費大量的人力物力怔檩,代價會比較大。
  2. 已經(jīng)改正了此前發(fā)布版本的bug蓄诽,如果下一個版本是一個大版本薛训,那么兩個版本的間隔時間會很長,這樣要等到下個大版本發(fā)布再修復bug仑氛,這樣此前版本的bug會長期的影響用戶乙埃。
  3. 版本升級率不高闸英,并且需要很長時間來完成版本覆蓋,此前版本的bug就會一直影響不升級版本的用戶介袜。
  4. 有一個小而重要的功能甫何,需要短時間內(nèi)完成版本覆蓋,比如節(jié)日活動遇伞。

為了解決上面的問題辙喂,熱修復框架就產(chǎn)生了。對于Bug的處理鸠珠,開發(fā)人員不要過于依賴熱修復框架巍耗,在開發(fā)的過程中還是要按照標準的流程做好自測、配合測試人員完成測試流程渐排。

2.熱修復框架的對比

熱修復框架的種類繁多炬太,按照公司團隊劃分主要有以下幾種:

類別 成員
阿里系 AndFix、Dexposed驯耻、阿里百川亲族、Sophix
騰訊系 微信的Tinker、QQ空間的超級補丁可缚、手機QQ的QFix
知名公司 美團的Robust霎迫、餓了么的Amigo、美麗說蘑菇街的Aceso
其他 RocooFix城看、Nuwa女气、AnoleFix

雖然熱修復框架很多,但熱修復框架的核心技術主要有三類测柠,分別是代碼修復、資源修復和動態(tài)鏈接庫修復缘滥,其中每個核心技術又有很多不同的技術方案轰胁,每個技術方案又有不同的實現(xiàn),另外這些熱修復框架仍在不斷的更新迭代中朝扼,可見熱修復框架的技術實現(xiàn)是繁多可變的赃阀。作為開發(fā)需需要了解這些技術方案的基本原理,這樣就可以以不變應萬變擎颖。

部分熱修復框架的對比如下表所示榛斯。

特性 AndFix Tinker/Amigo QQ空間 Robust/Aceso
即時生效
方法替換
類替換
類結構修改
資源替換
so替換
支持gradle
支持ART
支持Android7.0

我們可以根據(jù)上表和具體業(yè)務來選擇合適的熱修復框架,當然上表的信息很難做到完全準確搂捧,因為部分的熱修復框架還在不斷更新迭代驮俗。
從表中也可以發(fā)現(xiàn)Tinker和Amigo擁有的特性最多,是不是就選它們呢允跑?也不盡然王凑,擁有的特性多也意味著框架的代碼量龐大搪柑,我們需要根據(jù)業(yè)務來選擇最合適的,假設我們只是要用到方法替換索烹,那么使用Tinker和Amigo顯然是大材小用了工碾。另外如果項目需要即時生效,那么使用Tinker和Amigo是無法滿足需求的百姓。對于即時生效渊额,AndFix、Robust和Aceso都滿足這一點垒拢,這是因為AndFix的代碼修復采用了底層替換方案旬迹,而Robust和Aceso的代碼修復借鑒了Instant Run原理,現(xiàn)在我們就來學習代碼修復子库。

3.代碼修復

代碼修復主要有三個方案舱权,分別是底層替換方案、類加載方案和Instant Run方案仑嗅。

3.1 類加載方案

類加載方案基于Dex分包方案宴倍,什么是Dex分包方案呢?這個得先從65536限制和LinearAlloc限制說起仓技。
65536限制
隨著應用功能越來越復雜鸵贬,代碼量不斷地增大,引入的庫也越來越多脖捻,可能會在編譯時提示如下異常:

com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536

這說明應用中引用的方法數(shù)超過了最大數(shù)65536個阔逼。產(chǎn)生這一問題的原因就是系統(tǒng)的65536限制,65536限制的主要原因是DVM Bytecode的限制地沮,DVM指令集的方法調(diào)用指令invoke-kind索引為16bits嗜浮,最多能引用 65535個方法。
LinearAlloc限制
在安裝時可能會提示INSTALL_FAILED_DEXOPT摩疑。產(chǎn)生的原因就是LinearAlloc限制危融,DVM中的LinearAlloc是一個固定的緩存區(qū),當方法數(shù)過多超出了緩存區(qū)的大小時會報錯雷袋。

為了解決65536限制和LinearAlloc限制吉殃,從而產(chǎn)生了Dex分包方案。Dex分包方案主要做的是在打包時將應用代碼分成多個Dex楷怒,將應用啟動時必須用到的類和這些類的直接引用類放到主Dex中蛋勺,其他代碼放到次Dex中。當應用啟動時先加載主Dex鸠删,等到應用啟動后再動態(tài)的加載次Dex抱完,從而緩解了主Dex的65536限制和LinearAlloc限制。

Dex分包方案主要有兩種冶共,分別是Google官方方案乾蛤、Dex自動拆包和動態(tài)加載方案每界。因為Dex分包方案不是本章的重點,這里就不再過多的介紹家卖,我們接著來學習類加載方案眨层。
Android解析ClassLoader(二)Android中的ClassLoader中講到了ClassLoader的加載過程,其中一個環(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文件酪捡,因此每個dex文件對應一個Element叁征。
多個Element組成了有序的Element數(shù)組dexElements。當要查找類時逛薇,會在注釋1處遍歷Element數(shù)組dexElements(相當于遍歷dex文件數(shù)組)捺疼,注釋2處調(diào)用Element的findClass方法,其方法內(nèi)部會調(diào)用DexFile的loadClassBinaryName方法查找類永罚。如果在Element中(dex文件)找到了該類就返回啤呼,如果沒有找到就接著在下一個Element中進行查找。
根據(jù)上面的查找流程呢袱,我們將有bug的類Key.class進行修改官扣,再將Key.class打包成包含dex的補丁包Patch.jar,放在Element數(shù)組dexElements的第一個元素羞福,這樣會首先找到Patch.dex中的Key.class去替換之前存在bug的Key.class惕蹄,排在數(shù)組后面的dex文件中的存在bug的Key.class根據(jù)ClassLoader的雙親委托模式就不會被加載,這就是類加載方案治专,如下圖所示卖陵。


類加載方案需要重啟App后讓ClassLoader重新加載新的類,為什么需要重啟呢张峰?這是因為類是無法被卸載的赶促,因此要想重新加載新的類就需要重啟App,因此采用類加載方案的熱修復框架是不能即時生效的挟炬。
雖然很多熱修復框架采用了類加載方案,但具體的實現(xiàn)細節(jié)和步驟還是有一些區(qū)別的嗦哆,比如QQ空間的超級補丁和Nuwa是按照上面說得將補丁包放在Element數(shù)組的第一個元素得到優(yōu)先加載谤祖。微信Tinker將新舊apk做了diff,得到patch.dex老速,然后將patch.dex與手機中apk的classes.dex做合并粥喜,生成新的classes.dex,然后在運行時通過反射將classes.dex放在Element數(shù)組的第一個元素橘券。餓了么的Amigo則是將補丁包中每個dex 對應的Element取出來额湘,之后組成新的Element數(shù)組卿吐,在運行時通過反射用新的Element數(shù)組替換掉現(xiàn)有的Element 數(shù)組。

采用類加載方案的主要是以騰訊系為主锋华,包括微信的Tinker嗡官、QQ空間的超級補丁、手機QQ的QFix毯焕、餓了么的Amigo和Nuwa等等衍腥。

3.2 底層替換方案

與類加載方案不同的是,底層替換方案不會再次加載新類纳猫,而是直接在Native層修改原有類婆咸,由于是在原有類進行修改限制會比較多,不能夠增減原有類的方法和字段芜辕,如果我們增加了方法數(shù)尚骄,那么方法索引數(shù)也會增加,這樣訪問方法時會無法通過索引找到正確的方法侵续,同樣的字段也是類似的情況倔丈。
底層替換方案和反射的原理有些關聯(lián),就拿方法替換來說询兴,方法反射我們可以調(diào)用java.lang.Class.getDeclaredMethod乃沙,假設我們要反射Key的show方法,會調(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方法是個native方法眶根,對應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虛擬機中對應的一個ArtMethod指針蜀铲,ArtMethod結構體中包含了Java方法的所有信息,包括執(zhí)行入口属百、訪問權限记劝、所屬類和代碼執(zhí)行地址等等,ArtMethod結構如下所示族扰。
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結構中比較重要的字段是注釋1處的dex_cache_resolved_methods_和注釋2處的entry_point_from_quick_compiled_code_厌丑,它們是方法的執(zhí)行入口,當我們調(diào)用某一個方法時(比如Key的show方法)渔呵,就會取得show方法的執(zhí)行入口怒竿,通過執(zhí)行入口就可以跳過去執(zhí)行show方法。
替換ArtMethod結構體中的字段或者替換整個ArtMethod結構體扩氢,這就是底層替換方案耕驰。
AndFix采用的是替換ArtMethod結構體中的字段,這樣會有兼容問題录豺,因為廠商可能會修改ArtMethod結構體朦肘,導致方法替換失敗饭弓。Sophix采用的是替換整個ArtMethod結構體,這樣不會存在兼容問題媒抠。
底層替換方案直接替換了方法弟断,可以立即生效不需要重啟。采用底層替換方案主要是阿里系為主领舰,包括AndFix夫嗓、Dexposed、阿里百川冲秽、Sophix舍咖。

3.3 Instant Run方案

除了資源修復,代碼修復同樣也可以借鑒Instant Run的原理锉桑, 可以說Instant Run的出現(xiàn)推動了熱修復框架的發(fā)展排霉。
Instant Run在第一次構建apk時,使用ASM在每一個方法中注入了類似如下的代碼:

IncrementalChange localIncrementalChange = $change;//1
        if (localIncrementalChange != null) {//2
            localIncrementalChange.access$dispatch(
                    "onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
                            paramBundle });
            return;
        }

其中注釋1處是一個成員變量localIncrementalChange 民轴,它的值為$change攻柠,$change實現(xiàn)了IncrementalChange這個抽象接口。當我們點擊InstantRun時后裸,如果方法沒有變化則$change為null瑰钮,就調(diào)用return,不做任何處理微驶。如果方法有變化浪谴,就生成替換類,這里我們假設MainActivity的onCreate方法做了修改因苹,就會生成替換類MainActivity$override苟耻,這個類實現(xiàn)了IncrementalChange接口,同時也會生成一個AppPatchesLoaderImpl類扶檐,這個類的getPatchedClasses方法會返回被修改的類的列表(里面包含了MainActivity)凶杖,根據(jù)列表會將MainActivity的$change設置為MainActivity$override,因此滿足了注釋2的條件款筑,會執(zhí)行MainActivity$overrideaccess$dispatch方法智蝠,access$dispatch方法中會根據(jù)參數(shù)"onCreate.(Landroid/os/Bundle;)V"執(zhí)行MainActivity$override的onCreate方法,從而實現(xiàn)了onCreate方法的修改奈梳。
借鑒Instant Run的原理的熱修復框架有Robust和Aceso寻咒。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市颈嚼,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌饭寺,老刑警劉巖阻课,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件叫挟,死亡現(xiàn)場離奇詭異,居然都是意外死亡限煞,警方通過查閱死者的電腦和手機抹恳,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來署驻,“玉大人奋献,你說我怎么就攤上這事⊥希” “怎么了瓶蚂?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長宣吱。 經(jīng)常有香客問我窃这,道長,這世上最難降的妖魔是什么征候? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任杭攻,我火速辦了婚禮,結果婚禮上疤坝,老公的妹妹穿的比我還像新娘兆解。我一直安慰自己,他們只是感情好跑揉,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布锅睛。 她就那樣靜靜地躺著,像睡著了一般畔裕。 火紅的嫁衣襯著肌膚如雪衣撬。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天扮饶,我揣著相機與錄音具练,去河邊找鬼。 笑死甜无,一個胖子當著我的面吹牛扛点,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播岂丘,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼陵究,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了奥帘?” 一聲冷哼從身側(cè)響起铜邮,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后松蒜,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體扔茅,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年秸苗,在試婚紗的時候發(fā)現(xiàn)自己被綠了召娜。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡惊楼,死狀恐怖玖瘸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情檀咙,我是刑警寧澤雅倒,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站攀芯,受9級特大地震影響屯断,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜侣诺,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一殖演、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧年鸳,春花似錦趴久、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至膳算,卻和暖如春座硕,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背涕蜂。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工华匾, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人机隙。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓蜘拉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親有鹿。 傳聞我的和親對象是個殘疾皇子旭旭,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內(nèi)容