探索Android開(kāi)源框架 - 11. 熱修復(fù)原理

熱修復(fù)技術(shù)介紹

  • 重新發(fā)布版本代價(jià)大寂纪,成本高,不及時(shí)夭织,用戶體驗(yàn)差吭露,對(duì)此有幾種解決方案:
  1. Hybird:原生+H5混合開(kāi)發(fā),缺點(diǎn)是人工成本搞尊惰,用戶體驗(yàn)不如純?cè)桨负茫?/li>
  2. 插件化:移植成本高讲竿,對(duì)老代碼的改造費(fèi)時(shí)費(fèi)力,而且無(wú)法動(dòng)態(tài)修改弄屡;
  3. 熱修復(fù)技術(shù)题禀,將補(bǔ)丁上傳到云端,app可以直接從云端下來(lái)補(bǔ)丁直接應(yīng)用琢岩;
  • 熱修復(fù)技術(shù)對(duì)于國(guó)內(nèi)開(kāi)發(fā)者來(lái)說(shuō)是一個(gè)比較實(shí)用的功能投剥,可以解決如下問(wèn)題:
  1. 發(fā)布新版本代價(jià)較大,用戶下載安裝成本高担孔;
  2. 版本更新的效率問(wèn)題江锨,需要較長(zhǎng)時(shí)間來(lái)完成版本覆蓋;
  3. 版本更新的升級(jí)率問(wèn)題糕篇,不升級(jí)版本的用戶得不到修復(fù)啄育,強(qiáng)更又比較暴力。
  4. 小而重要的功能拌消,需要短時(shí)間內(nèi)完成版本覆蓋挑豌,比如節(jié)日活動(dòng)。
  • 熱修復(fù)的優(yōu)勢(shì):無(wú)需發(fā)版墩崩,用戶無(wú)感知氓英,修復(fù)成功率高,用時(shí)短鹦筹;
百家爭(zhēng)鳴的熱修復(fù)框架

熱修復(fù)技術(shù)原理

  • 熱修復(fù)框架的核心技術(shù)主要有三類补君,分別是代碼修復(fù)、資源修復(fù)和動(dòng)態(tài)鏈接庫(kù)修復(fù)

代碼修復(fù):

  • 代碼修復(fù)主要有三個(gè)方案昧互,分別是底層替換方案挽铁、類加載方案和Instant Run方案

1. 類加載方案

  • 類加載方案需要重啟App后讓ClassLoader重新加載新的類,因?yàn)轭愂菬o(wú)法被卸載的敞掘,要想重新加載新的類就需要重啟App叽掘,因此采用類加載方案的熱修復(fù)框架是不能即時(shí)生效的。
優(yōu)點(diǎn):
  • 不需要太多的適配玖雁;
  • 實(shí)現(xiàn)簡(jiǎn)單更扁,沒(méi)有諸多限制;
缺點(diǎn)
  • 需要APP重啟才能生效(冷啟動(dòng)修復(fù))茄菊;
  • dex插樁:Dalvik平臺(tái)存在插樁導(dǎo)致的性能損耗疯潭,Art平臺(tái)由于地址偏移問(wèn)題導(dǎo)致補(bǔ)丁包可能過(guò)大的問(wèn)題赊堪;
  • dex替換:Dex合并內(nèi)存消耗在vm head上面殖,可能OOM,導(dǎo)致合并失敗
  • 虛擬機(jī)在安裝期間為類打上CLASS_ISPREVERIFIED標(biāo)志是為了提高性能的哭廉,強(qiáng)制防止類被打上標(biāo)志會(huì)影響性能脊僚;
Dex分包
  • 類加載方案基于Dex分包方案,而Dex分包方案主要是為了解決65536限制和LinearAlloc限制:
  1. 65536限制:DVM指令集的方法調(diào)用指令invoke-kind索引為16bits,最多能引用 65535個(gè)方法;
  2. LinearAlloc限制:DVM中的LinearAlloc是一個(gè)固定的緩存區(qū)辽幌,當(dāng)方法數(shù)過(guò)多超出了緩存區(qū)的大小增淹,安裝時(shí)提示INSTALL_FAILED_DEXOPT;
  • Dex分包方案: 打包時(shí)將應(yīng)用代碼分成多個(gè)Dex,將應(yīng)用啟動(dòng)時(shí)必須用到的類和這些類的直接引用類放到主Dex中乌企,其他代碼放到次Dex中虑润。當(dāng)應(yīng)用啟動(dòng)時(shí)先加載主Dex,等到應(yīng)用啟動(dòng)后再動(dòng)態(tài)的加載次Dex加酵。主要有兩種方案拳喻,分別是Google官方方案、Dex自動(dòng)拆包和動(dòng)態(tài)加載方案猪腕。
ClassLoader
  • 在本系列的上一篇文章探索Android開(kāi)源框架 - 10. 插件化原理中有講過(guò)java中的ClassLoader(加載jar文件和Class文件冗澈,本質(zhì)是加載Class文件), android中的ClassLoader(加載dex文件和apk文件), 雙親委派機(jī)制,以及ClassLoader如何加載插件中的類陋葡,其實(shí)熱修復(fù)中代碼修復(fù)的類加載方案也是使用的同樣的原理亚亲;
幾種不同的實(shí)現(xiàn):
  1. 將補(bǔ)丁包放在Element數(shù)組的第一個(gè)元素得到優(yōu)先加載(QQ空間的超級(jí)補(bǔ)丁和Nuwa)
  2. 將補(bǔ)丁包中每個(gè)dex 對(duì)應(yīng)的Element取出來(lái),之后組成新的Element數(shù)組腐缤,在運(yùn)行時(shí)通過(guò)反射用新的Element數(shù)組替換掉現(xiàn)有的Element 數(shù)組(餓了么的Amigo);
  3. 將新舊apk做了diff捌归,得到patch.dex,然后將patch.dex與手機(jī)中apk的classes.dex做合并岭粤,生成新的classes.dex陨溅,然后在運(yùn)行時(shí)通過(guò)反射將classes.dex放在Element數(shù)組的第一個(gè)元素(微信Tinker)
  4. Sophix:dex的比較粒度在類的維度,并且 重新編排了包中dex的順序绍在,classes.dex,classes2.dex..,可以看作是 dex文件級(jí)別的類插樁方案,對(duì)舊包中的dex順序進(jìn)行打破重組

2. 底層替換方案

  • 其思想來(lái)源于Xposed框架门扇,完美詮釋了AOP編程,直接在Native層修改原有類(不需要重啟APP)偿渡,由于是在原有類進(jìn)行修改限制會(huì)比較多臼寄,不能夠增減原有類的方法和字段,因?yàn)檫@破壞原有類的結(jié)構(gòu)(引起索引變化), 雖然限制多溜宽,但時(shí)效性好吉拳,加載輕快,立即見(jiàn)效适揉;
優(yōu)點(diǎn)
  • 實(shí)時(shí)生效留攒,不需要重新啟動(dòng),加載輕快
缺點(diǎn)
  • 兼容性差嫉嘀,由于 Android 系統(tǒng)每個(gè)版本的實(shí)現(xiàn)都有差別炼邀,所以需要做很多的兼容。
  • 開(kāi)發(fā)需要掌握 jni 相關(guān)知識(shí), 而且native異常排查難度更高
  • 由于無(wú)法新增方法和字段剪侮,無(wú)法做到功能發(fā)布級(jí)別
幾種不同的實(shí)現(xiàn):
  1. 采用替換ArtMethod結(jié)構(gòu)體中的字段拭宁,這樣會(huì)有兼容問(wèn)題,因?yàn)槭謾C(jī)廠商的修改 以及 android版本的迭代可能會(huì)導(dǎo)致底層ArtMethod結(jié)構(gòu)的差異,導(dǎo)致方法替換失斀鼙辍兵怯;(AndFix)
  2. 同時(shí)使用類加載和底層替換方案,針對(duì)小修改腔剂,在底層替換方案限制范 圍內(nèi)媒区,還會(huì)再判斷所運(yùn)行的機(jī)型是否支持底層替換方案,是就采用底層替換(替換整個(gè)ArtMethod結(jié)構(gòu)體掸犬,這樣不會(huì)存在兼容問(wèn)題)驻仅,否則使用類加載替換;(Sophix)

3. Instant Run方案

Instant Run新特性的原理就是當(dāng)進(jìn)行代碼改動(dòng)之后登渣,會(huì)進(jìn)行增量構(gòu)建噪服,也就是僅僅構(gòu)建這部分改變的代碼,并將這部分代碼以補(bǔ)丁的形式增量地部署到設(shè)備上胜茧,然后進(jìn)行代碼的熱替換粘优,從而觀察到代碼替換所帶來(lái)的效果。其實(shí)從某種意義上講呻顽,Instant Run和熱修復(fù)在本質(zhì)上是一樣的雹顺。

Instant Run打包邏輯
  • 接入Instant Run之后,與傳統(tǒng)方式相比廊遍,在進(jìn)行打包的時(shí)候會(huì)存在以下四個(gè)不同點(diǎn)
  1. manifest注入:InstantRun會(huì)生成一個(gè)自己的application嬉愧,然后將這個(gè)application注冊(cè)到manifest配置文件里面,這樣就可以在其中做一系列準(zhǔn)備工作喉前,然后再運(yùn)行業(yè)務(wù)代碼没酣;
  2. nstant Run代碼放入主dex:manifest注入之后,會(huì)將Instant Run的代碼放入到Android虛擬機(jī)第一個(gè)加載的dex文件中卵迂,包括classes.dex和classes2.dex裕便,這兩個(gè)dex文件存放的都是Instant Run本身框架的代碼,而沒(méi)有任何業(yè)務(wù)層的代碼见咒。
  3. 工程代碼插樁——IncretmentalChange偿衰;這個(gè)插裝里面會(huì)涉及到具體的IncretmentalChange類。
  4. 工程代碼放入instantrun.zip改览;這里的邏輯是當(dāng)整個(gè)App運(yùn)行起來(lái)之后才回去解壓這個(gè)包里面的具體工程代碼下翎,運(yùn)行整個(gè)業(yè)務(wù)邏輯。
  • Instant Run在第一次構(gòu)建apk時(shí)宝当,使用ASM在每一個(gè)方法中注入了類似如下的代碼 (ASM 是一個(gè) Java 字節(jié)碼操控框架视事。它能被用來(lái)動(dòng)態(tài)生成類或者增強(qiáng)既有類的功能)
//$change實(shí)現(xiàn)了IncrementalChange這個(gè)抽象接口。
//當(dāng)點(diǎn)擊InstantRun時(shí)今妄,如果方法沒(méi)有變化則$change為null郑口,就調(diào)用return,不做任何處理盾鳞。
//如果方法有變化犬性,就生成替換類,假設(shè)MainActivity的onCreate方法做了修改,就會(huì)生成替換類MainActivity$override腾仅,
//這個(gè)類實(shí)現(xiàn)了IncrementalChange接口乒裆,同時(shí)也會(huì)生成一個(gè)AppPatchesLoaderImpl類,這個(gè)類的getPatchedClasses方法
//會(huì)返回被修改的類的列表(里面包含了MainActivity)推励,根據(jù)列表會(huì)將MainActivity的$change設(shè)置為MainActivity$override
//因此滿足了localIncrementalChange != null鹤耍,會(huì)執(zhí)行MainActivity$override的access$dispatch方法,
//access$dispatch方法中會(huì)根據(jù)參數(shù)”onCreate.(Landroid/os/Bundle;)V”執(zhí)行MainActivity$override的onCreate方法验辞,
//從而實(shí)現(xiàn)了onCreate方法的修改稿黄。
IncrementalChange localIncrementalChange = $change;
if (localIncrementalChange != null) {//2
    localIncrementalChange.access$dispatch(
            "onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
                    paramBundle });
    return;
}
被廢棄的Instant Run

Android Studio 3.5 中一個(gè)顯著變化是引入了 Apply Changes,它取代了舊的 Instant Run跌造。Instant Run 是為了更容易地對(duì)應(yīng)用程序進(jìn)行小的更改并測(cè)試它們杆怕,但它會(huì)產(chǎn)生一些問(wèn)題。為了解決這一問(wèn)題壳贪,谷歌已經(jīng)徹底刪除了 Instant Run陵珍,并從根本上構(gòu)建了 Apply Changes ,不再在構(gòu)建過(guò)程中修改 APK违施,而是使用運(yùn)行時(shí)工具動(dòng)態(tài)地重新定義類互纯,它應(yīng)該比立刻運(yùn)行更可靠和更快。

優(yōu)點(diǎn)
  • 實(shí)時(shí)生效磕蒲,不需要重新啟動(dòng)
  • 支持增加方法和類
  • 支持方法級(jí)別的修復(fù)留潦,包括靜態(tài)方法
  • 對(duì)每個(gè)產(chǎn)品代碼的每個(gè)函數(shù)都在編譯打包階段自動(dòng)的插入了一段代碼,插入過(guò)程對(duì)業(yè)務(wù)開(kāi)發(fā)是完全透明
缺點(diǎn)
  • 代碼是侵入式的辣往,會(huì)在原有的類中加入相關(guān)代碼
  • 會(huì)增大apk的體積

資源修復(fù):

  • 目前市面上大部分資源熱修復(fù)方案基本都參考了Instant Run的實(shí)現(xiàn)愤兵, 其主要分兩步:
  1. 創(chuàng)建新的AssetManager,并通過(guò)反射調(diào)用addAssetPath加載完整的新資源包排吴;
  2. 找到所有之前引用到原有AssetManager的地方秆乳,通過(guò)反射,把引用處 替換為新AssetManager钻哩;
  • 這里的具體原理可以參考章探索Android開(kāi)源框架 - 10. 插件化原理中的資源加載部分屹堰;
  • Sophix: 構(gòu)造了一個(gè)package id為0x66的資源包(原有資源包為 0x7f),此包只包含改變了的資源項(xiàng)街氢,然后直接在原有的AssetManager中 addAssetPath這個(gè)包就可以了扯键,不修改AssetManager的引用處,替換更快更安全

so庫(kù)修復(fù):

  • 主要是更新so珊肃,也就是重新加載so荣刑,主要用到了System的load和loadLibrary方法
  • System.load(""): 傳入so在磁盤的完整路徑馅笙,用于加載指定路徑的so
@CallerSensitive
public static void load(String filename) {
    Runtime.getRuntime().load0(Reflection.getCallerClass(), filename);
}
  • System.loadLibrary(""):傳入so名稱,用于加載app安裝后自動(dòng)從apk包中復(fù)制到/data/data/packagename/lib下的so
@CallerSensitive
public static void loadLibrary(String libname) {
    Runtime.getRuntime().loadLibrary0(Reflection.getCallerClass(), libname);
}
  • 最終都會(huì)調(diào)用到LoadNativeLibrary()厉亏,其主要做了如下工作:
  1. 判斷so文件是否已經(jīng)加載董习,若已經(jīng)加載判斷與class_Loader是否一樣,避免so重復(fù)加載爱只;
  2. 如果so文件沒(méi)有被加載皿淋,打開(kāi)so并得到so句柄,如果so句柄獲取失敗恬试,就返回false窝趣,常見(jiàn)新的SharedLibrary,如果傳入path對(duì)應(yīng)的library為空指針训柴,就將創(chuàng)建的SharedLibrary賦值給library哑舒,并將library存儲(chǔ)到libraries_中;
  3. 查找JNI_OnLoad的函數(shù)指針幻馁,根據(jù)不同情況設(shè)置was_successful的值散址,最終返回該was_successful;
兩種方案:
  1. 將so補(bǔ)丁插入到NativeLibraryElement數(shù)組的前部宣赔,讓so補(bǔ)丁的路徑先被返回和加載预麸;
  2. 調(diào)用System.load方法來(lái)接管so的加載入口;

參考

我是今陽(yáng),如果想要進(jìn)階和了解更多的干貨钩蚊,歡迎關(guān)注微信公眾號(hào) “今陽(yáng)說(shuō)” 接收我的最新文章

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末贡翘,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子砰逻,更是在濱河造成了極大的恐慌鸣驱,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蝠咆,死亡現(xiàn)場(chǎng)離奇詭異踊东,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)刚操,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門闸翅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人菊霜,你說(shuō)我怎么就攤上這事坚冀。” “怎么了鉴逞?”我有些...
    開(kāi)封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵记某,是天一觀的道長(zhǎng)司训。 經(jīng)常有香客問(wèn)我,道長(zhǎng)液南,這世上最難降的妖魔是什么壳猜? 我笑而不...
    開(kāi)封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮贺拣,結(jié)果婚禮上蓖谢,老公的妹妹穿的比我還像新娘捂蕴。我一直安慰自己譬涡,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布啥辨。 她就那樣靜靜地躺著涡匀,像睡著了一般。 火紅的嫁衣襯著肌膚如雪溉知。 梳的紋絲不亂的頭發(fā)上陨瘩,一...
    開(kāi)封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音级乍,去河邊找鬼舌劳。 笑死,一個(gè)胖子當(dāng)著我的面吹牛玫荣,可吹牛的內(nèi)容都是我干的甚淡。 我是一名探鬼主播,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼捅厂,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼贯卦!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起焙贷,我...
    開(kāi)封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤撵割,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后辙芍,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體啡彬,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年故硅,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了外遇。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡契吉,死狀恐怖跳仿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情捐晶,我是刑警寧澤菲语,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布妄辩,位于F島的核電站,受9級(jí)特大地震影響山上,放射性物質(zhì)發(fā)生泄漏眼耀。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一佩憾、第九天 我趴在偏房一處隱蔽的房頂上張望哮伟。 院中可真熱鬧,春花似錦妄帘、人聲如沸楞黄。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)鬼廓。三九已至,卻和暖如春致盟,著一層夾襖步出監(jiān)牢的瞬間碎税,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工馏锡, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留雷蹂,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓杯道,卻偏偏與公主長(zhǎng)得像匪煌,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子蕉饼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

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