熱修復(fù)技術(shù)介紹
- 重新發(fā)布版本代價(jià)大寂纪,成本高,不及時(shí)夭织,用戶體驗(yàn)差吭露,對(duì)此有幾種解決方案:
- Hybird:原生+H5混合開(kāi)發(fā),缺點(diǎn)是人工成本搞尊惰,用戶體驗(yàn)不如純?cè)桨负茫?/li>
- 插件化:移植成本高讲竿,對(duì)老代碼的改造費(fèi)時(shí)費(fèi)力,而且無(wú)法動(dòng)態(tài)修改弄屡;
- 熱修復(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)題:
- 發(fā)布新版本代價(jià)較大,用戶下載安裝成本高担孔;
- 版本更新的效率問(wèn)題江锨,需要較長(zhǎng)時(shí)間來(lái)完成版本覆蓋;
- 版本更新的升級(jí)率問(wèn)題糕篇,不升級(jí)版本的用戶得不到修復(fù)啄育,強(qiáng)更又比較暴力。
- 小而重要的功能拌消,需要短時(shí)間內(nèi)完成版本覆蓋挑豌,比如節(jié)日活動(dòng)。
- 熱修復(fù)的優(yōu)勢(shì):無(wú)需發(fā)版墩崩,用戶無(wú)感知氓英,修復(fù)成功率高,用時(shí)短鹦筹;
百家爭(zhēng)鳴的熱修復(fù)框架
手淘的Dexposed: 開(kāi)源铝阐,底層替換方案, 基于Xposed,針對(duì)Dalvik運(yùn)行時(shí)的Java Method Hook技術(shù)铐拐,但對(duì)于Dalvik底層過(guò)于依賴徘键,無(wú)法繼續(xù)兼容Android5.0之后的ART,因此作罷遍蟋;
支付寶的Andfix:開(kāi)源吹害,底層替換方案,借助Dexposed思想虚青,做到了Dalvik和ART環(huán)境的全版本兼容它呀,但其底層固定結(jié)構(gòu)的替換方案穩(wěn)定性不好,使用范圍也存在著諸多限制,而且對(duì)于資源和so修復(fù)未能實(shí)現(xiàn)纵穿,詳細(xì)原理參考:Android熱修復(fù)框架AndFix原理解析及使用烟号;
阿里百川的Hotfix:開(kāi)源,底層替換方案政恍,依賴于Andfix并對(duì)業(yè)務(wù)邏輯解耦汪拥,安全性和易用性較好,但還是存在Andfix的缺點(diǎn)篙耗;
Qzone超級(jí)補(bǔ)丁: 未開(kāi)源吮螺,類加載方案轴踱,會(huì)侵入打包流程
美團(tuán)的Robust:開(kāi)源,Instant Run方案,詳細(xì)可以參考美團(tuán)技術(shù)團(tuán)隊(duì)的文章及Robust源碼:Android熱更新方案Robust, Android熱更新方案Robust開(kāi)源肪获,新增自動(dòng)化補(bǔ)丁工具
大眾點(diǎn)評(píng)的Nuwa: 開(kāi)源旭绒,類加載方案龄糊,具體實(shí)現(xiàn)可以參考:Android 熱修復(fù)Nuwa的原理及Gradle插件源碼解析
餓了么的Amigo:開(kāi)源奴曙,類加載方案
微信的Tinker:開(kāi)源,類加載方案邓厕,關(guān)于Tinker的原理可以看一下鴻洋的文章:Android 熱修復(fù) Tinker接入及源碼淺析,
Android 熱修復(fù) Tinker 源碼分析之DexDiff / DexPatch, Android 熱修復(fù) Tinker Gradle Plugin解析手淘的Sophix:未開(kāi)源,商業(yè)收費(fèi)逝嚎,類加載方案+底層替換方案;(手淘團(tuán)隊(duì)基于Sophix有整理出一本電子書:深入探索Android熱修復(fù)技術(shù)原理, 其中不僅講了熱修復(fù)原理還有許多編譯相關(guān)的內(nèi)容(關(guān)注微信公眾號(hào)今陽(yáng)說(shuō)详恼,回復(fù)關(guān)鍵字"熱修復(fù)"領(lǐng)取書籍pdf))
熱修復(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限制:
- 65536限制:DVM指令集的方法調(diào)用指令invoke-kind索引為16bits,最多能引用 65535個(gè)方法;
- 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):
- 將補(bǔ)丁包放在Element數(shù)組的第一個(gè)元素得到優(yōu)先加載(QQ空間的超級(jí)補(bǔ)丁和Nuwa)
- 將補(bǔ)丁包中每個(gè)dex 對(duì)應(yīng)的Element取出來(lái),之后組成新的Element數(shù)組腐缤,在運(yùn)行時(shí)通過(guò)反射用新的Element數(shù)組替換掉現(xiàn)有的Element 數(shù)組(餓了么的Amigo);
- 將新舊apk做了diff捌归,得到patch.dex,然后將patch.dex與手機(jī)中apk的classes.dex做合并岭粤,生成新的classes.dex陨溅,然后在運(yùn)行時(shí)通過(guò)反射將classes.dex放在Element數(shù)組的第一個(gè)元素(微信Tinker)
- 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):
- 采用替換ArtMethod結(jié)構(gòu)體中的字段拭宁,這樣會(huì)有兼容問(wèn)題,因?yàn)槭謾C(jī)廠商的修改 以及 android版本的迭代可能會(huì)導(dǎo)致底層ArtMethod結(jié)構(gòu)的差異,導(dǎo)致方法替換失斀鼙辍兵怯;(AndFix)
- 同時(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)
- manifest注入:InstantRun會(huì)生成一個(gè)自己的application嬉愧,然后將這個(gè)application注冊(cè)到manifest配置文件里面,這樣就可以在其中做一系列準(zhǔn)備工作喉前,然后再運(yùn)行業(yè)務(wù)代碼没酣;
- nstant Run代碼放入主dex:manifest注入之后,會(huì)將Instant Run的代碼放入到Android虛擬機(jī)第一個(gè)加載的dex文件中卵迂,包括classes.dex和classes2.dex裕便,這兩個(gè)dex文件存放的都是Instant Run本身框架的代碼,而沒(méi)有任何業(yè)務(wù)層的代碼见咒。
- 工程代碼插樁——IncretmentalChange偿衰;這個(gè)插裝里面會(huì)涉及到具體的IncretmentalChange類。
- 工程代碼放入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)愤兵, 其主要分兩步:
- 創(chuàng)建新的AssetManager,并通過(guò)反射調(diào)用addAssetPath加載完整的新資源包排吴;
- 找到所有之前引用到原有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()厉亏,其主要做了如下工作:
- 判斷so文件是否已經(jīng)加載董习,若已經(jīng)加載判斷與class_Loader是否一樣,避免so重復(fù)加載爱只;
- 如果so文件沒(méi)有被加載皿淋,打開(kāi)so并得到so句柄,如果so句柄獲取失敗恬试,就返回false窝趣,常見(jiàn)新的SharedLibrary,如果傳入path對(duì)應(yīng)的library為空指針训柴,就將創(chuàng)建的SharedLibrary賦值給library哑舒,并將library存儲(chǔ)到libraries_中;
- 查找JNI_OnLoad的函數(shù)指針幻馁,根據(jù)不同情況設(shè)置was_successful的值散址,最終返回該was_successful;
兩種方案:
- 將so補(bǔ)丁插入到NativeLibraryElement數(shù)組的前部宣赔,讓so補(bǔ)丁的路徑先被返回和加載预麸;
- 調(diào)用System.load方法來(lái)接管so的加載入口;
參考
- Android進(jìn)階解密
- Android熱修復(fù)技術(shù)原理詳解
- Android 插件化和熱修復(fù)知識(shí)梳理
- 全面解析 Android 熱修復(fù)原理
- 干貨滿滿儒将,Android熱修復(fù)方案介紹
- 熱修復(fù)原理學(xué)習(xí)(6)資源熱修復(fù)技術(shù)
- Android熱修復(fù)升級(jí)探索——SO庫(kù)修復(fù)方案
- 深入探索Android熱修復(fù)技術(shù)原理(關(guān)注微信公眾號(hào)今陽(yáng)說(shuō)吏祸,回復(fù)關(guān)鍵字"熱修復(fù)"領(lǐng)取書籍pdf)