Android中熱修復框架Robust原理解析+並將框架程式碼從"閉源"變成"開源"(下篇)

weixin_34006468發表於2016-12-26

一、回顧框架原理
本篇繼續來看熱修復框架Robust原理,在之前的一篇文章中已經詳細講解了:Robust框架原理,因為這個框架不是開源的,所以通過官方給出的原理介紹,咋們自己模擬了案例和框架邏輯的簡單實踐。最後在通過反編譯美團app進行驗證咋們的邏輯實現是否大致不差。最終確定實踐的邏輯大同小異。但是在上一篇文章末尾多次強調了,這個框架吸引我研究的不是他熱修復技術,而是他有一個技術點,就是如何在編譯期給每個類每個方法都加上修復功能程式碼,對於上層開發程式碼是透明的。因為從之前案例可以看到,如果方法沒有修復功能程式碼,那麼此方法就喪失了修復功能,再來看一下這個框架的原理圖,包括編譯期動態插入程式碼和載入修復包邏輯:

2241150-8ab2d0d0be3981cf

二、自動插入原理分析
那麼下面就來詳細介紹編譯期這個框架是如何將專案中每個類每個方法都插入一段修復程式碼。在介紹這個知識點,可以先去了解一下,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類如下:

2241150-3df30307eb94c8d8
這個類非常簡單,定義了很多不同型別格式的方法,下面我們就要來編寫程式碼,自動給每個方法注入那段修復程式碼以及給這個類新增一個靜態變數。有了之前的那篇文章: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程式碼:


2241150-1713cdcdd77852e6

通過asm程式碼,我們需要注意的就是引數陣列構建,和返回值轉化:


2241150-af60c78359ee6486

下面我們可以把這段asm程式碼直接拷貝到Java程式碼中,在這個過程中,我們需要對那個引數陣列構建做處理了,因為現在方法的引數個數是不確定的,所以咋們得編寫動態構建程式碼:
2241150-14349e47520dfd6b

這段程式碼就是完成了修復程式碼的動態插入,邏輯和順序很清晰,首先得構造出方法的四個引數,其中最重要的就是第一個引數Object陣列了。

第一個引數:構建方法引數陣列


2241150-1667954346b56470

在這裡還得區分,一個方法是否為有引數和無引數的情況。做特殊處理,然後最核心的地方就是建立多個引數陣列型別的程式碼了:


2241150-ae0ab7031007b107

上面程式碼,就開始建立一個方法的所有引數型別陣列,需要做以下幾個特殊處理:
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程式碼即可:


2241150-506e98e1b2694727

這個方法就包含多個引數,而且所有特殊情況都包含了,檢視asm程式碼即可:
2241150-99d4c8ae24e5def5

這樣咋們就把坑給填完了。繼續看上面的程式碼,在處理特殊的基本型別,因為上面提到基本型別除了LOAD指令不一樣,還有就是需要進行物件封箱操作,從asm程式碼中也可以看到,看看具體方法:
2241150-6ceeedeb16de8ddd

對於不同基本型別做了特殊處理,下面看一下boolean型別的處理:
2241150-06b1ec86f5935bf3

其他基本型別都大致相同了,這裡不再解釋了。

第二個引數:當前方法所屬的類物件
到這裡就看完了,修復方法的第一個引數:物件陣列構建,也是整個過程中的核心,也是最複雜的。咋們在回過頭繼續看,第二個引數:方法當前所屬的物件


2241150-9b18ee29c220f313

這裡需要做判斷就是方法是否為static型別,如果是static型別直接傳入null即可,如果是非static型別就要直接傳入隱含的第一個引數this了。

第三個引數:靜態變數changeQuickRedirect
這個引數就簡單了,直接用類的靜態變數changeQuickRedirect即可:


2241150-1ccbdab608b6091c

第四個引數:方法是否為static型別


2241150-8b6cccfd2a20da09

有了上面四個引數之後,下面就可以開始呼叫了修復的兩個方法了,一個是isSupport:


2241150-c96c633d1a94aa8f

這個方法返回值是boolean型別,也就是在if語句中執行,可以用IFEQ指令即可。不過這裡還有一個坑,就是如果是Bytecode外掛直接得到的asm程式碼,方法的引數簽名第一個是Ljava/lang/Object;,這個明顯不對的,因為我們知道第一個引數是陣列型別,所以需要手動改成[Ljava/lang/Object;,這個坑找了好久才填成功了。

然後就是accessDispatch方法呼叫,在呼叫這個方法之前,我們依然需要構造四個引數,不過這個構造過程和之前是一模一樣的。直接抄過來就可以了,主要是執行完這個方法之後的事,又有好多坑:


2241150-efc712fe4650bb70

這裡看到,我們又得像上面構造那個複雜的方法引數陣列一樣填坑了。這裡需要做這幾個特殊處理:
1、方法是否有返回值,如果沒有返回值,直接呼叫Opcode.RETURN指令即可。
2、方法返回值型別如果是基本型別需要特殊處理。
3、方法返回值型別是物件型別,需要做型別簽名處理,如果是陣列型別不做處理,如果是非陣列型別需要去除前面的L字元,以及後面的分號字元,不然後面在使用dx命令轉化jar的時候報錯。
下面來看看如果返回值是基本型別,我們需要進行拆箱操作,即把物件型別變成基本型別:


2241150-eb96bac844a6eebd

程式碼也很簡單,直接拷貝asm程式碼即可,對每個基本型別做判斷即可。最後就是返回指令,因為不同基本型別和物件型別採用的不一樣,基本型別中float型別是FRETURN,long型別是LRETURN,double型別是DRETURN,其他型別都是IRETURN,如果是物件型別直接是ARETURN即可:
2241150-022972c85862d4d5

四、遇到的問題
到這裡我們就完成了動態程式碼注入的編寫,整個過程可以看到有很多地方需要處理,也就是填坑,在無數次實驗中遇到問題解決問題,因為如果開始把asm對應的程式碼拷貝過來會遇到一些問題的。不過每次遇到問題的時候解決辦法也很簡單,藉助jd-gui工具,檢視我們每次處理之後的class檔案,比如這裡:


2241150-1cd916a1f0a053b1

這裡看到,這個方法處理就報錯了,其實這個就是之前遇到的坑,如果一個引數前面一個引數是long,double型別沒有做特殊處理的結果。這時候發現有問題,我們可以先手動編寫修復程式碼,然後藉助Eclipse的Bytecode外掛檢視其對應的asm程式碼,和我們生成程式碼邏輯作比較即可。
還要一種方法,可以使用javap命令生成兩個class的位元組碼,然後對比也可以:


2241150-39903adb38ca4a7d

然後對比這兩個class檔案的位元組碼:
2241150-3507cdbc1ab853c0

不一樣的地方,再繼續修改指令即可。

五、踩過的坑
到這裡我們就把動態插入程式碼的邏輯編寫完畢了,總結一下我們遇到的坑:
第一、處理構造方法引數陣列
1、引數個數,位元組碼指令常數值是ICONST_0到ICONST_5,過了這個範圍,就得用BIPUSH指令。
2、基本型別需要進行封箱操作。
3、引數前面一個引數是long和double型別,需要做特殊處理。
4、基本型別和物件型別在存放值的時候用的LOAD指令不同。
第二、方法返回值處理
1、方法有無值返回。
2、返回值是基本型別需要做拆箱處理。
3、對於返回值是陣列和非陣列型別處理。
4、基本型別和物件型別返回指令不同。

六、包裝成小工具
下面還沒完,因為上面我們看到只是編寫完了插入程式碼的工具類方法,回頭可以看到,這個方法需要傳入幾個引數:

2241150-e79ad47bf5f9831b

下面來說明這幾個引數的含義:
第一個引數:操作方法的類MethodVisitor
第二個引數:方法所屬類的全稱名稱
第三個引數:方法引數簽名字串列表
第四個引數:方法返回值型別簽名
第五個引數:方法是否為static型別
下面咋們需要藉助asm包中的api來處理class檔案了,在之前介紹Android中動態插入程式碼工具icodetools 的時候,說過一句,操作類使用ClassVisitor,操作方法使用MethodVisitor即可,直接看程式碼:
2241150-2f55aef4efbbd4a9

這裡可以通過方法的描述欄位desc,通過Type類得到方法的引數型別和返回值型別
2241150-909a540c5608e604

在這裡,可以通過access欄位獲取方法是否為static型別,而且需要給每個類新增一個靜態變數changeQuickRedirect
2241150-612f1fb0ccb263d2

然後就需要藉助ClassReader類,這裡傳入的是需要處理的類的位元組陣列,然後可以獲取到類名。處理之後在返回類的位元組陣列即可。
外部在封裝一個方法,讀寫檔案,所這裡為了後面方便使用,編寫了兩個簡單小工具,一個是用於單獨class檔案處理,一個是為了jar檔案處理,只要輸入原始檔,輸出就是處理之後的結果了:
2241150-8f9b9cc0070d258d

這個專案中具體程式碼就不多解釋了,後面會給出專案的下載地址,可以自己弄下來慢慢解讀。但是這裡需要注意一點,就是這裡的ChangeQuickRedirect和PatchProxy這兩個類必須和應用工程中的名稱包名保持一致,不然插入是失敗的。下面就簡單用處理單個的class檔案工具處理一下Person類:
2241150-122db479040fdba7

好了,到這裡,咋們就處理完了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簽名

2241150-c8516185a746039f

那麼指令碼是我們自己控制的,所以可以在兩個階段選擇處理,也就有兩個方案:
第一個方案:只需要在將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了。最後小編週末寫文章真的好累,記得看完之後多多擴散分享,要是有打賞就更好了。

更多內容:點選這裡

關注微信公眾號,最新技術乾貨實時推送

2241150-74f5474319b0e710

掃一掃加小編微信新增時註明:“編碼美麗”否則不予通過!
2241150-3ccbc445b92827ad

2241150-e98d850ad8d71662

相關文章