阿里熱修復(fù)Sophix原理-筆記v0.8

我將熱修復(fù)原理落地實(shí)踐MyHotFix

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

1.1 什么是熱修復(fù)

為了修復(fù)剛發(fā)版時(shí)出現(xiàn)的緊急bug,無需重新發(fā)版!

1.2 技術(shù)積淀

手淘基于Xposed進(jìn)行改進(jìn)颜骤,產(chǎn)生針對(duì)Android Dalvik虛擬機(jī)的Java Method Hook技術(shù)的Dexposed宾濒。
支付寶提出AndFix方案,可以做到在Dalvik和Art全平臺(tái)兼容的即時(shí)修復(fù)
阿里百川結(jié)合手淘實(shí)際使用AndFix的經(jīng)驗(yàn)杨帽,解耦后推出HotFix方案
2017手淘聯(lián)合阿里云推出Sophix熱修復(fù)方案
其余著名熱修復(fù)方案有:
QQ空間超級(jí)補(bǔ)丁、微信Tinker
餓了么Amigo
美團(tuán)Robust
360RePlugin
滴滴出行VirtualAPK
uwa

2.代碼熱修復(fù)技術(shù)

2.1底層熱替換原理

AndFix:由補(bǔ)丁類的classLoader加載補(bǔ)丁類,在native層針對(duì)不同Android架構(gòu)中的不同的ArtMethod結(jié)構(gòu)調(diào)用對(duì)應(yīng)的replaceMethod方法按照定義好的ArtMethod結(jié)構(gòu)一一替換方法的所有信息如所屬類屹篓、訪問權(quán)限、代碼內(nèi)存地址等匙奴。
穩(wěn)定性較差堆巧,會(huì)受到國(guó)內(nèi)ROM廠商對(duì)ArtMethod結(jié)構(gòu)更改的影響,所以這正是AndFix不支持很多機(jī)型的原因。

Sophix:由補(bǔ)丁類的classLoader加載補(bǔ)丁類泼菌,在native層直接memcpy(smeth,dmth,sizeof(ArtMethod))替換整個(gè)artMethod的結(jié)構(gòu)谍肤。初始化類時(shí)會(huì)為這個(gè)類分配空間,AllocArtMethodArray會(huì)緊挨著的new出來放入art中的方法數(shù)組中灶轰。通過計(jì)算輔助類的前后兩個(gè)方法的起始地址就可以計(jì)算出artMethod結(jié)構(gòu)的大小了谣沸。
注:補(bǔ)丁類初始化時(shí),也會(huì)分配自己的artMethod空間笋颤,拿這個(gè)修復(fù)過的新ArtMethod去替換舊ArtMethod的內(nèi)容乳附,不用管ArtMethod的結(jié)構(gòu)。穩(wěn)定性大大提高伴澄!

猜測(cè):由于補(bǔ)丁類加載是從dex中加載赋除,故替換后的ArtMethod的方法入口首先應(yīng)該是dexCode解釋執(zhí)行,同步被優(yōu)化成oat機(jī)器碼非凌,下次執(zhí)行時(shí)就執(zhí)行oat機(jī)器碼入口了举农。

  • 訪問權(quán)限問題
    1.方法調(diào)用時(shí)權(quán)限檢查
    得益于在安裝時(shí)優(yōu)化成oat文件時(shí),已經(jīng)校驗(yàn)過了敞嗡。所以對(duì)同類的方法進(jìn)行調(diào)用時(shí)颁糟,不會(huì)再進(jìn)行權(quán)限檢查航背。
    2.同包名下權(quán)限問題
    同包名下調(diào)用熱修復(fù)之后的方法,會(huì)再次權(quán)限檢查棱貌,在native中的IsInSamePackage方法中判斷兩個(gè)類的classLoader是否相同玖媚,否則IllegalAcessError,因?yàn)檠a(bǔ)丁類是由補(bǔ)丁classLoader加載的,所以解決方法時(shí)婚脱,反射修改加載之后的補(bǔ)丁類的classLoader字段為舊classLoader今魔。
  • 反射調(diào)用非靜態(tài)方法產(chǎn)生的問題(依賴?yán)鋯?dòng)解決)
    反射調(diào)用非靜態(tài)方法時(shí),會(huì)調(diào)用底層的InvokeMethod,會(huì)調(diào)用VerifyObjectIsClass來判斷調(diào)用方法的對(duì)象是否是方法所屬類的實(shí)例障贸,然而由于熱替換的方法類還是執(zhí)行補(bǔ)丁類错森,所以校驗(yàn)失敗。
  • 即時(shí)生效帶來的限制
    兩種情況不適用篮洁,僅支持修復(fù)方法涩维,其他情況補(bǔ)丁小,修復(fù)快袁波。
    1.引起了原有類中結(jié)構(gòu)變化的修改
    2.修復(fù)的非靜態(tài)方法被反射調(diào)用

2.2你所不知道的Java

內(nèi)部類編譯

  • 靜態(tài)內(nèi)部類/非靜態(tài)內(nèi)部類區(qū)別
    內(nèi)部類會(huì)被編譯器生成同外部類一樣的頂級(jí)類激挪。只不過非靜態(tài)內(nèi)部類會(huì)持有外部類的引用。這也是Android性能優(yōu)化建議Handler使用靜態(tài)內(nèi)部類锋叨,防止外部類Activity不能被回收導(dǎo)致造成OOM垄分。
  • 內(nèi)部類和外部類互相訪問
    內(nèi)部類和外部類互相訪問private方法和字段時(shí),會(huì)自動(dòng)在對(duì)應(yīng)類為對(duì)方生成public的access&**方法娃磺。
  • 熱部署解決方案
    外部類如果有內(nèi)部類把所有的field/method的private訪問權(quán)限改成proteced或者public
    內(nèi)部類將所有的field/method的private訪問權(quán)限改成proteced或者public

匿名內(nèi)部類編譯

  • 匿名內(nèi)部類命名規(guī)則
    外部類&numble
    number即編譯器根據(jù)匿名內(nèi)部類出現(xiàn)在外部類中的順序薄湿,依次累加。
  • 熱部署解決方案
    新增/減少匿名內(nèi)部類對(duì)熱部署是無解的偷卧,因?yàn)檠a(bǔ)丁修復(fù)工具拿到的是class文件豺瘤,無法區(qū)別DexFileDemo&1和DexFileDemo&2,會(huì)導(dǎo)致類的順序亂套听诸。如果匿名內(nèi)部類插入到末尾是允許坐求。

有趣的域編譯

  • 靜態(tài)field,非靜態(tài)field編譯
    熱部署不支持field/method增加和刪除和<clinit>方法的修改
    靜態(tài)field的初始化和靜態(tài)代碼塊會(huì)被編譯在編譯器合成的方法<clinit>中
    非靜態(tài)字段的初始化會(huì)被編譯在編譯器生成的<init>無參構(gòu)造函數(shù)中
  • 靜態(tài)field,靜態(tài)代碼塊
    <clinit>方法會(huì)在類加載階段的類初始化時(shí)調(diào)用,<clinit>中靜態(tài)field和靜態(tài)代碼塊的出現(xiàn)順序就是二者在源碼中出現(xiàn)的順序晌梨。因?yàn)轭愐呀?jīng)加載過了桥嗤,所以就算修復(fù)了<clinit>方法也不會(huì)生效了。
    dvmResolveClass->dvmLinkClass->dvmInitClass,然后執(zhí)行clinit方法
    以下情況會(huì)去加載一個(gè)類
    1.new 一個(gè)類的對(duì)象時(shí)new instance
    2.調(diào)用類的靜態(tài)方法(invoke static)
    3.獲取類的靜態(tài)域的值(sget)
  • 非靜態(tài)field,非靜態(tài)代碼塊
    類的構(gòu)造函數(shù)會(huì)被編譯器翻譯成<init>方法仔蝌,會(huì)先進(jìn)行非靜態(tài)field和非靜態(tài)代碼塊的初始化泛领。它們出現(xiàn)的順序也是和在源碼中出現(xiàn)的順序一樣。
    執(zhí)行new instance指令時(shí)敛惊,如果類沒有加載過渊鞋,就嘗試加載類。然后對(duì)對(duì)象內(nèi)存分配,再然后執(zhí)行invoke direct指令調(diào)用類的init構(gòu)造函數(shù)進(jìn)行初始化
  • 熱部署解決方案
    不支持對(duì)靜態(tài)字段和靜態(tài)代碼塊的修改锡宋,會(huì)導(dǎo)致熱部署失敗儡湾,只能冷啟動(dòng)生效。支持非靜態(tài)字段和非靜態(tài)代碼塊修改执俩,熱部署只是將init構(gòu)造函數(shù)作為普通的方法變更盒粮。

final static 域編譯

  • final static 域編譯規(guī)則
    final static引用類型初始化仍在<clinit>中
    final static基本類型和String類型,類加載初始化dvminitClass在執(zhí)行clinit方法之前奠滑,先執(zhí)行initSFields,這個(gè)方法為static域賦予默認(rèn)值妒穴。引用類型默認(rèn)NULL宋税,final static修飾的基本類型和String類型會(huì)在這里初始化賦值。
  • final static 域優(yōu)化原理
    final static基本類型執(zhí)行const/4指令,操作數(shù)在dex中的位置(encoded_array_item)就是在opcode后一個(gè)字節(jié)讼油。
    final static String類型執(zhí)行const-string指令杰赛,本質(zhì)同上只不過拿到的是字符串常量在dex文件結(jié)構(gòu)中字符串常量區(qū)的索引id。dex文件有一塊區(qū)域存儲(chǔ)所有的字符串常量會(huì)被完整的加載到虛擬機(jī)內(nèi)存中-字符串常量區(qū)矮台。
    final static引用類型執(zhí)行sget指令,首先調(diào)用dvmDexGetResolveField看這個(gè)域是否之前解析過乏屯,沒有的話調(diào)用dvmDexResolveField嘗試解析域,如果這個(gè)靜態(tài)域所在的類沒有解析過瘦赫,嘗試調(diào)用dvmResolveClass,拿到這個(gè)sField辰晕,然后通過dvmDexGetResolveField(sField)獲取這個(gè)靜態(tài)值。
  • 熱部署解決方案
    final static基本類型/string類型最終引用的類型會(huì)被熱部署替換掉确虱。
    final static引用類型因?yàn)闀?huì)被翻譯到clinit方法中含友,熱部署失敗。

有趣的方法編譯

  • 應(yīng)用混淆方法編譯
    如果項(xiàng)目應(yīng)用了混淆校辩,會(huì)導(dǎo)致方法內(nèi)聯(lián)和裁剪最終導(dǎo)致Method新增/減少
  • 方法內(nèi)聯(lián)
    以下幾種情況下回發(fā)生方法內(nèi)聯(lián)(該方法會(huì)被刪除)
    1.方法沒有被任何地方引用
    2.方法僅在一處被引用窘问,調(diào)用方法的地方會(huì)被方法的實(shí)現(xiàn)替換掉
    3.方法太簡(jiǎn)單,僅僅一行語句宜咒。調(diào)用的語句也會(huì)被其實(shí)現(xiàn)所替換掉
    如果補(bǔ)丁修復(fù)的方法突然調(diào)用了原先只有一處被調(diào)用的方法惠赫,那么原先被內(nèi)聯(lián)掉的方法會(huì)新增出來,導(dǎo)致熱修復(fù)失敗故黑。
  • 方法裁剪(參數(shù)會(huì)被刪除)
    方法的參數(shù)沒有被引用過儿咱,該方法會(huì)被裁剪,然后再進(jìn)行混淆场晶。如果補(bǔ)丁方法再次調(diào)用這個(gè)參數(shù)就會(huì)導(dǎo)致新增方法概疆,那么只能走冷啟動(dòng)方案。
    可以采用走包裝類型Boolean判斷峰搪,簡(jiǎn)單調(diào)用該參數(shù)岔冀,保持對(duì)該參數(shù)的引用就不會(huì)被裁剪了
  • 熱部署解決方案
    只要混淆配置文件中-dontoptmize就不會(huì)做方法內(nèi)聯(lián)和裁剪了。所以不建議混淆時(shí)優(yōu)化代碼。
    而且因?yàn)锳ndroid執(zhí)行的是優(yōu)化后的dex文件使套,所以混淆中預(yù)校驗(yàn)在class文件中的優(yōu)勢(shì)就不存在了罐呼。

switch case語句編譯

  • switch case語句編譯規(guī)則
    編譯器會(huì)根據(jù)switch case的值是否連續(xù)分別生成不同的指令,packed-switch和sparse-switch指令侦高。如果packed有值不連續(xù)就用pswitch_0補(bǔ)齊 return-void嫉柴。
  • 熱部署解決方案
    在sophix進(jìn)行資源補(bǔ)丁包時(shí),需要對(duì)引用的資源進(jìn)行替換奉呛,如果swith case語句恰好被編譯成packed-switch指令則可能會(huì)漏掉计螺。解決方法是修改打補(bǔ)丁包時(shí)的smail反編譯流程,碰到packed-switch指令強(qiáng)轉(zhuǎn)為sparse-switch指令,:pswitch_N等標(biāo)簽指令也需要被替換成:sswitch_N指令瞧壮,然后做資源Id替換登馒,編程smail為dex

泛型編譯

  • 為什么需要泛型
    Java泛型完全有編譯器實(shí)現(xiàn),由編譯器執(zhí)行類型檢查和類型推斷咆槽,生成非泛型字節(jié)碼陈轿,稱之為擦除。
    沒有泛型之前想要實(shí)現(xiàn)類泛型秦忿,利用所有類的父類時(shí)Object進(jìn)行強(qiáng)轉(zhuǎn)麦射,這完全依賴程序員的自主性,很容易出現(xiàn)ClassCastException灯谣。泛型的出現(xiàn)解決了類型檢查和類型推斷的問題潜秋。
  • 泛型類型擦除
    Java字節(jié)碼中不包含泛型類型信息,想要區(qū)別類型定義可以限定泛型類型 <T extends Number>
  • 類型擦除與多態(tài)的沖突和解決
    父類是泛型類有setNumber(T value),子類想override setNumber(Number value)胎许。然而實(shí)際父類的方法實(shí)際是setNumber(Object value),子類想重寫卻變成了重載半等,這就出現(xiàn)了類型擦除和多態(tài)之間的沖突。然而編譯器自動(dòng)幫我們合成了Bridge方法實(shí)現(xiàn)了重載呐萨,在子類中生成了相同簽名bridge方法杀饵,內(nèi)部實(shí)際調(diào)用子類的重寫方法。
  • 泛型類型轉(zhuǎn)換
    編譯器如果發(fā)現(xiàn)變量聲明加上了泛型信息谬擦,編譯器自動(dòng)加上了check-cast的強(qiáng)制轉(zhuǎn)換切距,因?yàn)榫幾g器會(huì)為泛型做類型檢查,所以自動(dòng)的強(qiáng)制轉(zhuǎn)換不會(huì)出現(xiàn)ClassCastException惨远。
  • 熱部署解決方案
    如果父類補(bǔ)丁變成了增加了泛型則會(huì)增加Bridge方法谜悟,造成熱部署失敗。
    將方法從void get(B t) 變成<B extends Number> void get(B t)方法邏輯不會(huì)發(fā)生變化北秽,但是方法的簽名會(huì)發(fā)生變化葡幸,這種情況熱修復(fù)沒有意義,需要避免這種情況的發(fā)生贺氓。

Lambda表達(dá)式編譯

  • Lambda表達(dá)式編譯規(guī)則
    Lamda表達(dá)式具有函數(shù)式編程的特點(diǎn)蔚叨,是Java中最接近閉包的概念。函數(shù)式接口:一個(gè)接口具有唯一一個(gè)抽象方法
    Java中的Runable和Comparator都是典型的函數(shù)式接口
    Lamada表達(dá)式和匿名內(nèi)部類的區(qū)別:
    1.this關(guān)鍵字指包圍Lamada表達(dá)式的類而不是指向匿名內(nèi)部類自己
    2.編譯方式,Java編譯器將Lamda表達(dá)式編譯成類的私有方法蔑水,使用了Java7的invokedynamic動(dòng)態(tài)綁定這個(gè)私有方法邢锯。而匿名內(nèi)部類則是生成外部類&number的新類
    編譯器都會(huì)在類下生成lamda$main$**{ * }私有靜態(tài)方法,這個(gè)方法實(shí)現(xiàn)了lamda表達(dá)式的邏輯搀别,引用的變量都會(huì)變成方法的參數(shù)丹擎。
    在HostSpot VM下解釋class文件的lamda表達(dá)式:
    invokeDynamic指令調(diào)用java/lang/invoke/LamdaMetafactory的metafactory這個(gè)靜態(tài)方法。這個(gè)方法會(huì)在運(yùn)行時(shí)生成實(shí)現(xiàn)函數(shù)式接口的具體類歇父,這個(gè)具體類會(huì)調(diào)用那個(gè)靜態(tài)私有方法蒂培。
    在Android虛擬機(jī)下解釋dex文件中的lamda表達(dá)式:則是在優(yōu)化成dex文件的時(shí)候就生成了這個(gè)具體類。
  • 熱部署解決方案
    新增lamada表達(dá)式會(huì)導(dǎo)致外部類新增一個(gè)輔助方法榜苫。修改的lamda表達(dá)式邏輯引用了外部變量护戳,會(huì)導(dǎo)致輔助類持有了外部對(duì)象,會(huì)新增這個(gè)外部對(duì)象的變量单刁。也是會(huì)導(dǎo)致熱修復(fù)失敗。

訪問權(quán)限檢查對(duì)熱替換的影響

  • 類加載階段父類/實(shí)現(xiàn)接口訪問檢查
    一個(gè)類的加載階段包括resolve->link->init三個(gè)階段府适,父類/實(shí)現(xiàn)接口 權(quán)限檢查在link階段羔飞,dvmLinkClass中依次對(duì)父類/實(shí)現(xiàn)接口進(jìn)行dvmCheckClassAcess權(quán)限檢查,如果父類/實(shí)現(xiàn)接口是非public然后進(jìn)行檢查當(dāng)前類和它是否是相同的ClassLoader檐春。熱修復(fù)補(bǔ)丁類是新classLoader加載的逻淌,所在會(huì)報(bào)父類不允許訪問的錯(cuò)誤。
  • 類校驗(yàn)階段訪問權(quán)限檢查
    如果訪問public類和方法在類加載階段會(huì)通過疟暖,但是在運(yùn)行時(shí)會(huì)爆出crash異常卡儒。
    補(bǔ)丁在單個(gè)dex文件中,加載dex肯定要進(jìn)行dexopt,再dexopt過程中會(huì)dvmVerifyClass校驗(yàn)dex每個(gè)類俐巴。在校驗(yàn)過程中會(huì)檢查補(bǔ)丁類所引用類的訪問權(quán)限(提前dvmResolveClass被調(diào)用類)骨望。還會(huì)校驗(yàn)調(diào)用方法的訪問權(quán)限,public修飾直接返回欣舵。protected的話擎鸠,先檢查當(dāng)前類和被調(diào)用方法所屬類是否是父子類關(guān)系,不是的話會(huì)調(diào)用dvmIsSmaePackage,這里會(huì)判斷是否是相同的classLoader缘圈。...

<clinit>方法

該方法無法被熱修復(fù),只能走冷啟動(dòng)劣光。

2.3冷啟動(dòng)類加載原理

類啟動(dòng)方案實(shí)現(xiàn)概述

QQ空間超級(jí)補(bǔ)丁采用的插樁方式,入侵打包流程糟把,單獨(dú)放一個(gè)幫助類在獨(dú)立的dex中讓其他類調(diào)用绢涡,阻止類在dexopt時(shí)被打傷CLASS_ISPREVERIFIED標(biāo)記。加載補(bǔ)丁dex得到dexFile對(duì)象作為參數(shù)構(gòu)建一個(gè)Element對(duì)象插入到dexElement數(shù)組最前面遣疯。
Tinker提供差量包雄可,整體替換dex的方案。將patch.dex與應(yīng)用的class.dex合并生成一個(gè)完整的dex,加載完整的dex得到dexFile對(duì)象為參數(shù)構(gòu)建一個(gè)Element對(duì)象替換dexElements數(shù)組滞项。
官方multiDex
沒有補(bǔ)丁查詢更新狭归,下載補(bǔ)丁待下次啟動(dòng)時(shí)生效。

插樁實(shí)現(xiàn)前因后果

插樁方案是在dalvik和art下通用的冷啟動(dòng)方案文判。
將dex加載到本地內(nèi)存時(shí)过椎,如果不存在odex文件,首先會(huì)進(jìn)行dexopt戏仓。dexopt會(huì)調(diào)用verify/optimize操作疚宇。Apk第一次安裝時(shí),會(huì)對(duì)dex執(zhí)行dexopt赏殃,如果只存在一個(gè)dex文件則dvmVerifyClass(class)會(huì)打上CLASS_IS_PREVERIFIED標(biāo)記敷待。然后執(zhí)行dvmOptimizeClass(class)打上CLASS_IS_OPTIMIZED標(biāo)記。
  • dvmVerifyClass:防止類被篡改校驗(yàn)類的合法性仁热。我們主要關(guān)心會(huì)校驗(yàn)類的所有方法直接引用類和當(dāng)前類是否屬于同一個(gè)dex

  • dvmOptimizeClass:類優(yōu)化榜揖,將部分指令優(yōu)化成虛擬機(jī)內(nèi)部指令,如invoke-=>invoke--quick抗蠢。quick指令會(huì)直接從vtable中獲取方法地址,vtable是記錄了類的所有方法举哟。加快執(zhí)行速度。

     補(bǔ)丁類存在獨(dú)立的dex中迅矛,類A訪問補(bǔ)丁類的方法妨猩。嘗試解析dvmResolveClass補(bǔ)丁類時(shí),會(huì)判斷referrer類打上標(biāo)記然后校驗(yàn)referrer類和當(dāng)前類是否是同一個(gè)dex秽褒,拋出dvmIllegeThrowAccessException壶硅。
     為了解決問題,通常的做法是入侵打包流程销斟,利用class字節(jié)碼修改技術(shù)在每個(gè)類的構(gòu)造函數(shù)中引用獨(dú)立dex中的幫助類庐椒。防止Apk安裝時(shí)被打上CLASS_IS_PREVERFIED標(biāo)記,因此解決了這個(gè)異常問題蚂踊。
     類在運(yùn)行時(shí)初始化時(shí)沒打上標(biāo)記還會(huì)在執(zhí)行一次verifyClass/optimizeClass扼睬。dvmInitClass完成父類初始化,當(dāng)前類初始化以及static字段初始化賦值悴势。dvmVerifyClass非常重窗宇,對(duì)類方法的所有指令進(jìn)行校驗(yàn),雖然單個(gè)類影響不大特纤,但應(yīng)用加載大量類時(shí)军俊,會(huì)導(dǎo)致非常耗性能。
    

插樁導(dǎo)致類加載性能影響

在應(yīng)用啟動(dòng)場(chǎng)景下會(huì)加載大量的類捧存,啟動(dòng)時(shí)容易出現(xiàn)白屏粪躬。

避免插樁的QFix方案

在native層提前調(diào)用dvmResolveClass担败,是的在dvmResolve中調(diào)用dvmDexGetResolve不為null,也避免了校驗(yàn)一致性的問題镰官。
這個(gè)方案要求傳遞的在多dex情況下提前,referrer類必須跟patch類是同一個(gè)dex。fromUnverifiedConstant必須為true泳唠。referrer必須提前加載狈网。
這方案還要一些問題,在dexopt之后繞過笨腥,但是dexopt會(huì)改變很多原先的邏輯拓哺,許多odex層面的優(yōu)化會(huì)寫死字段和訪問方法的偏移。這會(huì)造成很嚴(yán)重的BUG脖母。

Art下冷啟動(dòng)實(shí)現(xiàn)

除了通用的插樁方案士鸥,針對(duì)Art下的冷啟動(dòng)方案實(shí)現(xiàn)如下。
為了使熱部署和冷啟動(dòng)共用一個(gè)補(bǔ)丁谆级,熱部署模式下的補(bǔ)丁能夠降級(jí)直接走冷啟動(dòng)烤礁,所以不需要dex merge。而Thinker為了解決Art下odex地址偏移寫死的問題肥照,合并成一個(gè)全新完整的dex得到dexElement整體替換舊的dexElements脚仔。

  • Dalvik-dexload:在加載原dex文件和jar壓縮文件中只會(huì)把classes.dex文件加載到內(nèi)存,然后會(huì)生成odex文件。
  • Art-dexload:打開壓縮文件加載多個(gè)dex文件建峭,首先加載的是primaryDex(classes.dex)玻侥,后續(xù)加載其他dex文件决摧。方法調(diào)用鏈:DexFile_openDexFileNative->openDexFileFromOat(從oat文件中加載)->LoadDexFiles(從壓縮文件中加載)

補(bǔ)丁類的dex只需要改成classes.dex亿蒸,其他原dex文件依次更名為classes(2,3...).dex,一起打包一個(gè)壓縮文件掌桩。先加載classes.dex的補(bǔ)丁類就不會(huì)再加載其他dex中的補(bǔ)丁類了边锁。

不得不說的其他點(diǎn)

DexFile.loadDex會(huì)將一個(gè)dex加載到內(nèi)存,在加載到內(nèi)存之前波岛,如果dex不存在對(duì)應(yīng)odex,在Dalvik下會(huì)執(zhí)行dexopt茅坛,Art下會(huì)執(zhí)行dexoat。
如果dex足夠大则拷,會(huì)導(dǎo)致dexopt/dexoat很耗時(shí)贡蓖。針對(duì)Art啟動(dòng)耗時(shí)的方案(感覺這里有誤,art下優(yōu)化文件應(yīng)該是oat而不是odex,loadDex方案贊同)煌茬,sophix方案在art下dexopt處理的是patch.dex和原理的dex的壓縮包斥铺,所以dexoat非常耗時(shí)。所以如果優(yōu)化后的odex沒有生成就不能夠在應(yīng)用啟動(dòng)的時(shí)候loadDex坛善,只能開子線程loadDex晾蜘。將loadDex看做事務(wù)邻眷,一旦打斷就刪除odex,生成odex之后剔交,如果應(yīng)用重啟發(fā)現(xiàn)生成了odex文件就loadDex肆饶,然后就反射替換dexElements數(shù)組。如果不存在就重啟另外一個(gè)子線程loadDex岖常,重啟之后再生效驯镊。
還會(huì)對(duì)補(bǔ)丁進(jìn)行簽名校驗(yàn)和對(duì)odex進(jìn)行md5校驗(yàn),不對(duì)就重新生成腥椒,防止文件被篡改阿宅。

完整方案的考慮

在補(bǔ)丁加載類之前的類是無法被修復(fù)的(Application類)
在Dalvik采用阿里自己研發(fā)的全量dex方案
在art下僅僅是將差量補(bǔ)丁包作為primaryDex(classes.dex)加載

2.4多態(tài)對(duì)冷啟動(dòng)加載的影響

重新認(rèn)識(shí)多態(tài)

多態(tài)用的是動(dòng)態(tài)綁定技術(shù),在運(yùn)行期判斷引用對(duì)象的實(shí)際類型笼蛛,根據(jù)實(shí)際類型調(diào)用方法洒放。動(dòng)態(tài)指非靜態(tài)方法和非私有方法即public/proteced/default,字段沒有多態(tài)滨砍。
那么Android中是如何實(shí)現(xiàn)多態(tài)呢往湿?
Android對(duì)動(dòng)態(tài)綁定技術(shù)做了優(yōu)化,在類初始化時(shí)一次性動(dòng)態(tài)綁定好惋戏,然后在運(yùn)行時(shí)調(diào)用該類的virtual方法直接讀取方法引用领追,調(diào)用就可以了。
具體實(shí)現(xiàn)是:
初始化時(shí)
1.為類創(chuàng)建一個(gè)vtable表响逢,vtable實(shí)際是記錄類的所有virtual方法的方法指針绒窑。
2.確定vtable數(shù)組的最大大小為父類vtable的大小+子類virutal方法數(shù)
3.初始化vtable內(nèi)容,將父類的vtable復(fù)制到子類的vtable中
4.遍歷子類的vitual方法舔亭,判斷方法的原型是否和vtable中一樣些膨,相同則重寫該方法實(shí)際引用
5.如果方法原型不同則將子類的virtual方法添加到vtable中的末尾
調(diào)用時(shí)
1.類方法invoke virtual指令在Android dex第一次加載時(shí)執(zhí)行dexopt的dvmOptimizeClass中會(huì)被優(yōu)化成invoke-virtual-quick虛擬機(jī)內(nèi)部指令
2.確定當(dāng)前變量的實(shí)際類型,quick指令會(huì)直接從類的vtable中獲取方法指針進(jìn)行執(zhí)行钦铺,加f快執(zhí)行速度
sget/invoke static指令簡(jiǎn)述
從當(dāng)前變量的引用類型找订雾,而不是實(shí)際類型。找不到就遞歸從父類中查找

冷啟動(dòng)方案限制

QFix方法在Davik下矛洞,dex執(zhí)行dexopt的dvmOptimize時(shí)將invoke-virtual指令優(yōu)化成了invoke-virtual-quick指令洼哎。指令后面跟的立即數(shù)就是方法在vtable中索引值(這里有個(gè)問題,我們從上面知道類在初始化時(shí)才會(huì)確定vtable中各子類的索引值沼本。而dexopt不沒有初始化類噩峦,猜測(cè)是提前使用了動(dòng)態(tài)綁定的分析方法確定了vtable的索引值)。
patch類新增類virtual方法抽兆,如果patch類是父類识补,則該父類加載之后就會(huì)使得子類中繼承父類vtable中索引值混亂了。比如子類原本重寫了父類的A方法郊丛,而修復(fù)之后的父類的B方法在A方法之前李请,實(shí)際就是A方法重寫了新增的B方法瞧筛,在想要調(diào)用子類對(duì)象的中父類新增的B方法時(shí)卻調(diào)用被覆蓋的A方法。

終極方案

QFix方法不可行导盅,而Tinker將多個(gè)dex合并成完整的dex方案可以解決這個(gè)問題较幌。
google開源了dexmerge方法,將補(bǔ)丁dex和原dex合并成一個(gè)完整的dex,但是多個(gè)dex合并會(huì)有65536的問題白翻,而且還會(huì)非常消耗內(nèi)存乍炉,內(nèi)存不足會(huì)導(dǎo)致合并失敗。2.5會(huì)說明完美方案滤馍。

2.5 Dalvik下完整DEX方案的新探索

冷啟動(dòng)類加載修復(fù)

QQ空間

  • dex插入方案岛琼,將補(bǔ)丁dex插入到classLoader索引路徑最前面
  • 通過插樁方式繞過Dalvik虛擬機(jī)下類的pre-verify問題

QFix

  • 同是dex插入方案
  • 通過在Jni層提前resolve所有補(bǔ)丁類繞過pre-verify檢查

Tinker:

  • 全量合成新dex,消除重復(fù)class重復(fù)帶來的沖突
  • 所有類加載都在一個(gè)dex完成巢株,沒有pre-verify問題

一種新的全量dex方案

在基線包里去除掉補(bǔ)丁包里的class后槐瑞,然后將其他的dex都load進(jìn)來「蟀基線包可以找到補(bǔ)丁中新類困檩,補(bǔ)丁包中新類也可以找到基線包中不變的類。這樣基線包中不變的class仍然可以按照舊的邏輯odex那槽,最大程度保證了dexopt的效果悼沿。
在dex文件中類的入口在DexHeader中表現(xiàn)為class_defs。遍歷pHeader->classDefsOff偏移值獲取DexClassDef骚灸,如果發(fā)現(xiàn)這個(gè)類名存在補(bǔ)丁中就從pClassDefs數(shù)組中移除糟趾,重新排列,修改classDefsSize甚牲。這樣修改不去移除類的定義等信息义郑,雖然會(huì)殘留類等信息,但是會(huì)提高dex的處理速度鳖藕。

對(duì)于Application的處理

Sophix在Applicatoin類加載補(bǔ)丁之后魔慷,清除Applicatoin類的pre_verified標(biāo)記只锭,使得跨dex加載類不會(huì)報(bào)異常著恩。不侵入編譯流程,不進(jìn)行反射蜻展,在運(yùn)行期自動(dòng)做好喉誊。
Tinker要求開發(fā)者聲明TinkerApplication,將真正的Application作為參數(shù)傳入TinkerApplication纵顾,應(yīng)用啟動(dòng)時(shí)啟動(dòng)Tinker自己的熱修復(fù)邏輯伍茄,在聲明周期回調(diào)時(shí)通過反射執(zhí)行原來的Application邏輯。
Amigo在編譯過程中通過gradle插件將Application替換成它的Application施逾,修復(fù)完成之后調(diào)用舊Application的attach(context),最后調(diào)用舊Application中的oncarete()調(diào)用原來的邏輯敷矫。
清除pre_verified邏輯:在jni層清除標(biāo)記 clazzObj->accessFlags=~CLASS_PREVERIFIED.

dvmOptResolveClass問題與對(duì)策

清除標(biāo)記遇到問題例获。多dex應(yīng)用時(shí),如果在Application類沒有被打上pre_verified標(biāo)記曹仗,那么虛擬機(jī)在初始化類時(shí)會(huì)掃描其所有用到的類進(jìn)行dvmOptResolveClass操作榨汤,這個(gè)方法對(duì)類進(jìn)行初始化加載的是原dex的類,那么這些類就會(huì)被打上pre_verified標(biāo)記怎茫。補(bǔ)丁加載之后收壕,只對(duì)Application類進(jìn)行了清除標(biāo)記操作。那些打上標(biāo)記的類調(diào)用補(bǔ)丁類的話還是會(huì)出現(xiàn)pre_verified問題轨蛤。
繞過dvmOptResovleClass操作蜜宪,僅僅讓Application類被打上標(biāo)記的解決方案
  • 讓Application用到的非系統(tǒng)類和Application在同一個(gè)dex中,保證打上pre_verified標(biāo)記祥山,避免進(jìn)入dvmOptResolveClass操作圃验。(Android官方的multi-dex就是將Application用到的類都打包到主dex中)
  • 讓Application類除了熱修復(fù)代碼之外,其他代碼剝離開放到一個(gè)單獨(dú)的類缝呕。Application通過反射調(diào)用其他類损谦,是的Application徹底與其他類獨(dú)立開,保證被打上pre_verified標(biāo)記岳颇。

3資源熱修復(fù)技術(shù)

3.1普遍的實(shí)現(xiàn)方式

InstantRun實(shí)現(xiàn)分兩步

  • 構(gòu)造新的AssetManager,反射調(diào)用addAssetPath添加新的完整的資源包路徑到AssetManager
  • 找到之前所有引用到原有的AssetManager的地方照捡,通過反射將引用處替換為新的AssetManager

一個(gè)Android進(jìn)程只有一個(gè)AssetManager, AssetManager->mResources->mResTable,從資源包路徑解析出resource.arsc(記錄了所有資源id分配情況和資源中所有字符串,以二進(jìn)制信息存儲(chǔ))话侧,將其相關(guān)信息存儲(chǔ)到mResTable.mPackageGroups(解析過的所有資源包的集合)

3.2資源文件的格式

resource.arsc由一個(gè)個(gè)ResChunk拼接起來栗精,ResChunk頭部都是一個(gè)ResChunk_header結(jié)構(gòu) (指示了大小和類型)
每個(gè)Android資源信息都是一個(gè)32為的編號(hào)0xPPTTEEEE,PP為packageId,TT為typeid,EEEE為entryid.

  • packageid對(duì)應(yīng)ResTable_package結(jié)構(gòu)體的id
  • typeid對(duì)應(yīng)ResTable_typeSpec結(jié)構(gòu)體的id,typeid對(duì)應(yīng)的具體類型需要到package chrunk里的String Pool解析到。
  • 每個(gè)entry表示一個(gè)資源項(xiàng)瞻鹏,資源項(xiàng)是按照排列的先后順序被自動(dòng)標(biāo)記編號(hào)悲立。

3.3運(yùn)行時(shí)資源的解析

應(yīng)用運(yùn)行第一行代碼時(shí),系統(tǒng)已經(jīng)構(gòu)建好AssetManager,包含系統(tǒng)包資源id為0x01,安裝包資源0x7f,aapt打包應(yīng)用資源packageid都是為0x7f.
如果在這個(gè)原assetManager基礎(chǔ)上添加補(bǔ)丁包資源新博。
Android KK下薪夕。addAssetPath 只將補(bǔ)丁包路徑添加到mAssetPath中,而在此之前資源包已經(jīng)被解析過了赫悄,所以補(bǔ)丁資源包不會(huì)生效原献。
在Android L上。因?yàn)閜ackageid相同埂淮,會(huì)添加同一個(gè)packageGroup下姑隅。補(bǔ)丁資源會(huì)添加到Type資源集合后面。獲取某個(gè)type資源的時(shí)候會(huì)從前往后遍歷倔撞,新補(bǔ)丁資源則仍不會(huì)生效讲仰。
所以InstantRun方案,需要一個(gè)全新的AssetManager痪蝇,添加一個(gè)完整的資源包鄙陡,替換掉原來的AssetManager.

3.4另辟蹊徑的資源修復(fù)方案

采用差量資源包冕房,補(bǔ)丁足夠小,不入侵打包流程趁矾。
構(gòu)建差量的0x66的補(bǔ)丁資源包毒费,只包含改變之后的資源項(xiàng)。然后直接在原有的AssetManager中addAssetPath這個(gè)資源包愈魏。并修正補(bǔ)丁包中引用的資源id觅玻,舊資源id發(fā)生變化的id。

3.5更優(yōu)雅的替換AssetManager

在Android L以后的版本直接在原來的AssetManager添加資源包就行了培漏。
在Android kk下溪厘,調(diào)用native層銷毀AssetManager資源,將java層的AssetManager指向native層的AssetManager設(shè)置為空牌柄,native層的AssetManager的析構(gòu)函數(shù)會(huì)釋放之前加載的所有資源畸悬。java層的AssetManager成為空殼之后就可以重新初始化了。初始化之后我們?cè)貯ddAssetPath就會(huì)生效了珊佣。

sophix的資源修復(fù)蹋宦,需要代碼修復(fù)支持,也只在新代碼中生效咒锻。

4.SO庫熱修復(fù)技術(shù)

4.1SO庫加載原理

so庫加載句柄冷冗,從so庫中加載方法映射到內(nèi)存中的hash表中,此時(shí)方法代碼應(yīng)該被加載到內(nèi)存中惑艇。

4.2SO庫熱部署實(shí)時(shí)生效可行性分析

動(dòng)態(tài)注冊(cè):解析映射native方法蒿辙,第一次走原so包,第二次走補(bǔ)丁包
靜態(tài)注冊(cè):加載補(bǔ)丁包之后滨巴,解注冊(cè)靜態(tài)的jni native方法思灌,重新映射

4.3SO庫冷部署重啟生效實(shí)現(xiàn)方案

art:在解析nativeLibaryDirectorys中插在最前面 補(bǔ)丁包
dalvik:重命名補(bǔ)丁包,同樣方法

4.4如何正確復(fù)制補(bǔ)丁SO庫

熱修復(fù)框架集成文檔

sophix集成
tinker集成

面試熱修復(fù)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末恭取,一起剝皮案震驚了整個(gè)濱河市泰偿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蜈垮,老刑警劉巖耗跛,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異窃款,居然都是意外死亡课兄,警方通過查閱死者的電腦和手機(jī)牍氛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門晨继,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人搬俊,你說我怎么就攤上這事紊扬⊙亚眩” “怎么了?”我有些...
    開封第一講書人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵餐屎,是天一觀的道長(zhǎng)檀葛。 經(jīng)常有香客問我,道長(zhǎng)腹缩,這世上最難降的妖魔是什么屿聋? 我笑而不...
    開封第一講書人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮藏鹊,結(jié)果婚禮上润讥,老公的妹妹穿的比我還像新娘。我一直安慰自己盘寡,他們只是感情好楚殿,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著竿痰,像睡著了一般脆粥。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上影涉,一...
    開封第一講書人閱讀 49,749評(píng)論 1 289
  • 那天变隔,我揣著相機(jī)與錄音,去河邊找鬼蟹倾。 笑死弟胀,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的喊式。 我是一名探鬼主播孵户,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼岔留!你這毒婦竟也來了夏哭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤献联,失蹤者是張志新(化名)和其女友劉穎竖配,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡宙地,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年辫塌,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片胁镐。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出盯漂,到底是詐尸還是另有隱情颇玷,我是刑警寧澤,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布就缆,位于F島的核電站帖渠,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏竭宰。R本人自食惡果不足惜空郊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望切揭。 院中可真熱鬧渣淳,春花似錦、人聲如沸伴箩。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嗤谚。三九已至棺蛛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間巩步,已是汗流浹背旁赊。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留椅野,地道東北人终畅。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像竟闪,于是被迫代替她去往敵國(guó)和親离福。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

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