標籤:注入 對比 bat 代碼 data- ack word 外掛程式 大小
一、回顧架構原理
本篇繼續來看熱修複架構Robust原理,在之前的一篇文章中已經詳細講解了:Robust架構原理,因為這個架構不是開源的,所以通過官方給出的原理介紹,咋們自己類比了案例和架構邏輯的簡單實踐。最後在通過反編譯美團app進行驗證咋們的邏輯實現是否大致不差。最終確定實踐的邏輯大同小異。但是在上一篇文章末尾多次強調了,這個架構吸引我研究的不是他熱修複技術,而是他有一個技術點,就是如何在編譯期給每個類每個方法都加上修複功能代碼,對於上層開發代碼是透明的。因為從之前案例可以看到,如果方法沒有修複功能代碼,那麼此方法就喪失了修複功能,再來看一下這個架構的原理圖,包括編譯期動態插入代碼和載入修複包邏輯:
二、自動插入原理分析
那麼下面就來詳細介紹編譯期這個架構是如何將項目中每個類每個方法都插入一段修複代碼。在介紹這個知識點,可以先去瞭解一下,Java中如何利用asm包操作位元組碼邏輯。或者可以看一下這篇文章:Android中動態插入代碼工具icodetools 這篇文章中已經詳細介紹了如何在每個類的每個方法中插入一段代碼。其實本文就是基於這個技術來進行操作的。不過這裡插入的代碼比那個要複雜。不多解釋了,直接來看怎麼操作。
為了示範和填坑方便,咋們最好開始使用一個簡單的案例來,因為第一次誰都保證不了能一帆風順插入成功。所以這裡就用一個簡單的類檔案進行即可。這裡定義一個簡單的類Person,內部定義多個不同類型的方法,包括方法的傳回值,參數,類型等。這也是為了後續檢測我們插入代碼的各種情形是否都能成功。我們的目的也只有一個,就是如何動態給Person這個類中每個方法插入之前提到的動態代碼:
if(changeQuickRedirect != null){if(PatchProxy.isSupport(new Object[]{xxx,xxx,...}, this, changeQuickRedirect, false)){return ((XXX) PatchProxy.accessDispatch(new Object[0], this, changeQuickRedirect, false));}}
在類中插入一個靜態變數:
public static ChangeQuickRedirect changeQuickRedirect;
咋們定義的Person類如下:
這個類非常簡單,定義了很多不同類型格式的方法,下面我們就要來編寫代碼,自動給每個方法注入那段修複代碼以及給這個類添加一個靜態變數。有了之前的那篇文章:Android中動態插入代碼工具icodetools,我們操作就很簡單了,這裡依然需要藉助asm包和Eclipse的外掛程式Bytecode,咋們直接利用Bytecode外掛程式查看那段代碼的asm對應的代碼,不過這裡需要注意,每個方法插入的代碼不同,先來看修複代碼的兩個重要方法:isSupport和accessDispatch,這兩個方法都有四個參數:
第一個參數:Object數組,存放的是這個方法的所有參數值,看到如果是基本類型需要做封箱轉化。
第二個參數:當前方法所屬的對象,如果方法是static類型就是null,如果方法是非static的就是this。
第三個參數:修複介面類型,也就是我們需要插入的靜態變數changeQuickRedirect。
第四個參數:方法是否為static類型。
所以從上面這四個參數,就知道我們在插入代碼時需要做如下處理,主要包括以下幾點:
1、每個方法的參數不同,因為我們看到插入的修複代碼的isSupport和accessDispatch方法的第一個參數都是一個Object數組,也就是這個方法的所有參數。
2、方法型別宣告不同,如果一個方法是static類型的,isSupport和accessDispatch方法的第二個參數是null,以及最後一個參數是true,否則就是this和false值。
3、方法的傳回值不同,對於方法是否有傳回值需要做特殊處理,以及方法傳回值類型不同也要做處理。
主要是這三點,但是實際操作還有很多小的細節問題,比如參數如果是基本類型,咋們還得做封箱操作,將其變成物件類型。傳回值如果是基本類型,還得做拆箱操作,把物件類型變成基本類型。
三、自動插入案例
上面分析完了基本原理,下面直接來操作,開始我們用一個簡單的方法做案例,然後手動的先插入一段修複代碼,在藉助Bytecode外掛程式查看這段代碼對應的asm代碼:
通過asm代碼,我們需要注意的就是參數數組構建,和傳回值轉化:
下面我們可以把這段asm代碼直接拷貝到Java代碼中,在這個過程中,我們需要對那個參數數組構建做處理了,因為現在方法的參數個數是不確定的,所以咋們得編寫動態構建代碼:
這段代碼就是完成了修複代碼的動態插入,邏輯和順序很清晰,首先得構造出方法的四個參數,其中最重要的就是第一個參數Object數組了。
第一個參數:構建方法參數數組
在這裡還得區分,一個方法是否為有參數和無參數的情況。做特殊處理,然後最核心的地方就是建立多個參數數群組類型的代碼了:
上面代碼,就開始建立一個方法的所有參數類型數組,需要做以下幾個特殊處理:
1、因為位元組碼指令中常量值指令是Opcode.ICONST_0到Opcode.ICONST_5的,所以如果一個數組大小超過這個指令範圍了,就得藉助Opcode.BIPUSH進行操作了。
2、判斷當前方法是否為static類型的,因為這個類型關係到後面取方法局部參數的索引值,我們知道非static類型的方法有一個隱含的參數this,所以這裡要做一次局部參數索引值判斷。static類型從0開始,非static類型從1開始。
3、在進行數組資料填充的時候,因為需要通過索引值訪問,這裡依然要做特殊處理,超過5通過Opcode.BIPUSH指令進行操作了。
4、對於參數處理需要區分基本類型和物件類型,因為他們採用的LOAD指令不同,一般基本類型中long是LLOAD,double是DLOAD,float是FLOAD,其他基本類型都是ILOAD;對於物件類型都是ALOAD。
5、對於參數中,如果一個參數的前面一個參數是long,double類型,要對參數索引做特殊處理,這裡猜想可能和這兩種類型佔用的位元組數有關,畢竟他們都是佔用8個位元組。而其他類型都是在4個位元組以內的。當遇到是這種兩種類型,參數索引值就得加一。
看到這裡有這麼多個坑,可以想到我在填坑的時候多麼痛苦,但是填坑方法也是很簡單的,可以先類比定義這樣的方法,然後查看他對應的asm代碼即可:
這個方法就包含多個參數,而且所有特殊情況都包含了,查看asm代碼即可:
這樣咋們就把坑給填完了。繼續看上面的代碼,在處理特殊的基本類型,因為上面提到基本類型除了LOAD指令不一樣,還有就是需要進行對象封箱操作,從asm代碼中也可以看到,看看具體方法:
對於不同基本類型做了特殊處理,下面看一下boolean類型的處理:
其他基本類型都大致相同了,這裡不再解釋了。
第二個參數:當前方法所屬的類對象
到這裡就看完了,修複方法的第一個參數:對象數組構建,也是整個過程中的核心,也是最複雜的。咋們在回過頭繼續看,第二個參數:方法當前所屬的對象
這裡需要做判斷就是方法是否為static類型,如果是static類型直接傳入null即可,如果是非static類型就要直接傳入隱含的第一個參數this了。
第三個參數:靜態變數changeQuickRedirect
這個參數就簡單了,直接用類的靜態變數changeQuickRedirect即可:
第四個參數:方法是否為static類型
有了上面四個參數之後,下面就可以開始調用了修複的兩個方法了,一個是isSupport:
這個方法傳回值是boolean類型,也就是在if語句中執行,可以用IFEQ指令即可。不過這裡還有一個坑,就是如果是Bytecode外掛程式直接得到的asm代碼,方法的參數簽名第一個是Ljava/lang/Object;,這個明顯不對的,因為我們知道第一個參數是數群組類型,所以需要手動改成[Ljava/lang/Object;,這個坑找了好久才填成功了。
然後就是accessDispatch方法調用,在調用這個方法之前,我們依然需要構造四個參數,不過這個構造過程和之前是一模一樣的。直接抄過來就可以了,主要是執行完這個方法之後的事,又有好多坑:
這裡看到,我們又得像上面構造那個複雜的方法參數數組一樣填坑了。這裡需要做這幾個特殊處理:
1、方法是否有傳回值,如果沒有傳回值,直接調用Opcode.RETURN指令即可。
2、方法傳回值類型如果是基本類型需要特殊處理。
3、方法傳回值類型是物件類型,需要做類型簽名處理,如果是數群組類型不做處理,如果是非數群組類型需要去除前面的L字元,以及後面的分號字元,不然後面在使用dx命令轉化jar的時候報錯。
下面來看看如果傳回值是基本類型,我們需要進行拆箱操作,即把物件類型變成基本類型:
代碼也很簡單,直接拷貝asm代碼即可,對每個基本類型做判斷即可。最後就是返回指令,因為不同基本類型和物件類型採用的不一樣,基本類型中float類型是FRETURN,long類型是LRETURN,double類型是DRETURN,其他類型都是IRETURN,如果是物件類型直接是ARETURN即可:
四、遇到的問題
到這裡我們就完成了動態代碼注入的編寫,整個過程可以看到有很多地方需要處理,也就是填坑,在無數次實驗中遇到問題解決問題,因為如果開始把asm對應的代碼拷貝過來會遇到一些問題的。不過每次遇到問題的時候解決辦法也很簡單,藉助jd-gui工具,查看我們每次處理之後的class檔案,比如這裡:
這裡看到,這個方法處理就報錯了,其實這個就是之前遇到的坑,如果一個參數前面一個參數是long,double類型沒有做特殊處理的結果。這時候發現有問題,我們可以先手動編寫修複代碼,然後藉助Eclipse的Bytecode外掛程式查看其對應的asm代碼,和我們產生代碼邏輯作比較即可。
還要一種方法,可以使用javap命令產生兩個class的位元組碼,然後對比也可以:
然後對比這兩個class檔案的位元組碼:
不一樣的地方,再繼續修改指令即可。
五、踩過的坑
到這裡我們就把動態插入代碼的邏輯編寫完畢了,總結一下我們遇到的坑:
第一、處理構造方法參數數組
1、參數個數,位元組碼指令常數值是ICONST_0到ICONST_5,過了這個範圍,就得用BIPUSH指令。
2、基本類型需要進行封箱操作。
3、參數前面一個參數是long和double類型,需要做特殊處理。
4、基本類型和物件類型在存放值的時候用的LOAD指令不同。
第二、方法傳回值處理
1、方法有無值返回。
2、傳回值是基本類型需要做拆箱處理。
3、對於傳回值是數組和非數群組類型處理。
4、基本類型和物件類型返回指令不同。
六、封裝成小工具
下面還沒完,因為上面我們看到只是編寫完了插入代碼的工具類方法,回頭可以看到,這個方法需要傳入幾個參數:
下面來說明這幾個參數的含義:
第一個參數:操作方法的類MethodVisitor
第二個參數:方法所屬類的全稱名稱
第三個參數:方法參數簽名字串列表
第四個參數:方法傳回值類型簽名
第五個參數:方法是否為static類型
下面咋們需要藉助asm包中的api來處理class檔案了,在之前介紹Android中動態插入代碼工具icodetools 的時候,說過一句,操作類使用ClassVisitor,操作方法使用MethodVisitor即可,直接看代碼:
這裡可以通過方法的描述欄位desc,通過Type類得到方法的參數類型和傳回值類型
在這裡,可以通過access欄位擷取方法是否為static類型,而且需要給每個類添加一個靜態變數changeQuickRedirect
然後就需要藉助ClassReader類,這裡傳入的是需要處理的類的位元組數組,然後可以擷取到類名。處理之後在返回類的位元組數組即可。
外部在封裝一個方法,讀寫檔案,所這裡為了後面方便使用,編寫了兩個簡單小工具,一個是用於單獨class檔案處理,一個是為了jar檔案處理,只要輸入源檔案,輸出就是處理之後的結果了:
這個項目中具體代碼就不多解釋了,後面會給出項目的,可以自己弄下來慢慢解讀。但是這裡需要注意一點,就是這裡的ChangeQuickRedirect和PatchProxy這兩個類必須和應用工程中的名稱包名保持一致,不然插入是失敗的。下面就簡單用處理單個的class檔案工具處理一下Person類:
好了,到這裡,咋們就處理完了Robust架構動態插入代碼的邏輯了。提供了兩個工具,一個是處理jar檔案,一個是處理單獨class檔案。那麼有同學可能會困惑?美團項目應該還有其他動作吧。的確如此。
七、項目中如何使用工具
有了這兩個工具,我們可以將其匯出成jar檔案,在項目編譯期間開始操作,先不管項目用ant指令碼,還是gradle指令碼了,不瞭解用指令碼編譯Android應用的同學,可以查看這裡:Android中使用指令碼編譯應用;用指令碼編譯項目都是需要經曆這麼幾個階段的:
1、使用Android SDK提供的aapt.exe產生R.java類檔案
2、使用Android SDK提供的aidl.exe把.aidl轉成.java檔案(如果沒有aidl,則跳過這一步)
3、使用JDK提供的javac.exe編譯.java類檔案產生class檔案
4、使用Android SDK提供的dx.bat命令列指令碼產生classes.dex檔案
5、使用Android SDK提供的aapt.exe產生資源套件檔案(包括res、assets、androidmanifest.xml等)
6、使用Android SDK提供的apkbuilder.bat產生未簽名的apk安裝檔案
7、使用jdk的jarsigner.exe對未簽名的包進行apk簽名
那麼指令碼是我們自己控制的,所以可以在兩個階段選擇處理,也就有兩個方案:
第一個方案:只需要在將java檔案用javac命令編譯成class檔案之後,利用上面的那個可以處理單個class檔案工具進行處理即可。這樣對於開發人員其實是無感知的。在編譯階段自動完成了。
第二個方案:在編譯所有檔案得到class檔案之後,將其打包成jar檔案,然後在藉助上面提到的處理jar檔案工具進行處理即可。然後在使用dx命令將處理之後的jar檔案變成dex檔案即可。
其實不管是哪種方案,只要在編譯期找對時機,利用上面給出的兩個工具都可以完成的。其實還有一種思路,就是需要藉助之前提到的icodetools工具,需要把這個工具進行改一下,把本文中的動態插入代碼邏輯移植到icodetools工具中,然後咋們可以輸入一個apk,輸出的apk就是已經添加成功的結果了。不過這種方式不可取,我相信美團不會用這種思路去處理的。
八、最佳化工作
到這裡我們就算把美團的Robust架構中動態插入修複代碼的邏輯講解完了,但是這裡還有一些細節問題需要處理:
1、添加黑名單規則,我們可以看到,這個動態插入程式碼片段是為了修複作用,那麼一個apk中所有類是否都有必要插入呢?明顯不需要,比如我們用到了v4包中的類,那麼這裡的類肯定不需要插入的。當然還有一些我們自己定義的類的一些方法也不想插入的。所以這裡就要有一個插入時的黑名單,這個需要在上面插入工具裡做處理,比較簡單,因為我們知道處理的方法名和類名了,只是做一個簡單過濾即可。
2、從上面看到每個類需要有一個changeQuickRedirect變數,這個變數名是唯一的,但是又不能保證在開發過程中,每個開發人員都會使用這個名字,如果有人使用了,而我們又自動插入了,那麼編譯肯定會報錯的。所以我們在插入代碼之前需要做一些判斷邏輯。如果有這個變數就不插入了。並且給與一些資訊提示。
九、架構優缺點
結合之前的架構原理實踐案例以及本文的知識,下面來看一下美團這個熱修複架構的優缺點:
優點:在之前一篇文章中已經知道他的載入邏輯非常簡單,直接使用DexClassLoader類載入修複包即可。所以可以看到這個修複架構的相容性非常好。因為直接使用系統提供的api,不會有很高的崩潰率,不像AndFix架構藉助底層,會有系統限制需要做相容操作的。
缺點:從本文就可以知道了,一個企業級應用代碼本身就很龐大,在這樣給每個類每個方法都插入了這段代碼那麼,可想而知,插入代碼之後的apk包得多大。而且還有一些混淆問題,和AndFix架構一樣,不支援資源修複。
所以如果把這個架構真正整合到項目中還有很多坑需要我們去填,當然這個不是本文介紹的範圍了。感興趣的同學可以去網上搜一下關於Robust架構的問題,有詳細說明。熱修複路慢慢其修遠兮,吾將上下而填坑!
項目:https://github.com/fourbrother/RobustInsertCodeTools
十、總結
本文主要繼續前面一篇文章介紹Robust架構的原理和實踐案例之後,看一下這個架構的核心技術點就是如何在編譯期間自動給每個類每個方法中插入代碼,藉助asm包和Bytecode外掛程式完成了。而這個意義不僅僅是局限於研究了Robust架構,而是為了後續操作都有用,也就是說以後如果有自動插入代碼邏輯,本文也是一個非常不錯的案例。後面還會繼續分析市面上的最後一個熱修複架構Tinker了。最後小編周末寫文章真的好累,記得看完之後多多擴散分享,要是有打賞就更好了。
更多內容:點擊這裡
關注公眾號,最新技術乾貨即時推送
掃一掃加小編
添加時註明:“編碼美麗”否則不予通過!
Android中熱修複架構Robust原理解析+並將架構代碼從"閉源"變成"開源"(下篇)