關於Android APP在線熱修復bug方案的調研(一)(AndFix)

調研背景:

     App發佈出去後,如果發現有緊急或重要bug如何進行修復呢?

     重新發布一版APK?但這樣代價太大....

      那麼有沒有一種方案能夠不用更新整個APK,而只把服務器上的很小的補丁文件下載下來進行修復bug呢?

      本文的調研也正是爲了解決該問題。


幾種解決方案對比:

      下面是找到的3款比較火的開源解決方案,分別都是利用不同原理實現的:

名稱

優點

缺點

適用場景

實現原理與下載

DynamicAPK

1.支持插件化開發,並且Plugin APK能夠訪問Host APK的資源

1.需要修改aapt工具,改變原來的編譯流程。

2.不支持Hot fix(官方介紹有文字提到支持,但只是load的時候考慮了下,還有很多工作沒有實現)

 

適用於Plugin開發

【實現原理】

這是攜程網開源的,Multidex+AAPT編譯流程改造。參考:http://www.infoq.com/cn/articles/ctrip-android-dynamic-loading?utm_campaign=infoq_content&



【源碼下載】

https://github.com/CtripMobile/DynamicAPK

 

AndFix

1.Mexdex方案相比,性能要好些。(Multi Dex需要修改所有classclass_ispreverified標誌位,導致運行時性能有所損失)


2.
支持ARTDalvik

3.
支持6.0

1.跳過了類初始化,對於靜態或者構造函數或者class.forname()的處理可能會有問題

在線修復bug

【實現原理】

阿里的開源項目,原理是函數Hook。GitHub上有介紹。



【源碼下載】

https://github.com/alibaba/AndFix

Nuwa

(HotFix/DroidFix)

 

1.兼容性比AndFix(Multi dex方案,沒有static的問題)

2.支持ARTDalvik



3.支持6.0

 

 1.編譯完成java代碼後,需要遍歷修改class文件,插入代碼,防止class被打上class_ispreverified標誌,這會導致運行性能降低


 

在線修復bug

 【實現原理】

Multidex的動態加載原理,參考:http://bugly.qq.com/blog/?p=781



【源碼下載】
https://github.com/jasonross/Nuwa


 結合我的目標,目標鎖定在第2個與第3個。


AndFix的操作使用】

選擇第二個AndFix來進行研究:

第一步:模擬發佈APK:

使用自帶的sample中的列子,修了下onCreate()中的代碼,如下:



安裝運行bug.apk,如預期輸出如下Log:


第二步:現在App發佈出去後有Bug啦,我們要改變onCreate()的輸出,趕緊開始製作patch.....

修改輸出代碼爲如下:



編譯出fix後的APK,這時需要使用AndFix/tools/下面的apkpatch這個工具來製作patch(補丁)文件。

工具所在目錄:


工具使用說明:

ApkPatch v1.0.3 - a tool for build/merge Android Patch file (.apatch).
Copyright 2015 supern lee <[email protected]>

usage: apkpatch -f <new> -t <old> -o <output> -k <keystore> -p <***> -a <alias> -e <***>
 -a,--alias <alias>     alias.
 -e,--epassword <***>   entry password.
 -f,--from <loc>        new Apk file path.
 -k,--keystore <loc>    keystore path.
 -n,--name <name>       patch name.
 -o,--out <dir>         output dir.
 -p,--kpassword <***>   keystore password.
 -t,--to <loc>          old Apk file path.

usage: apkpatch -m <apatch_path...> -k <keystore> -p <***> -a <alias> -e <***>
 -a,--alias <alias>     alias.
 -e,--epassword <***>   entry password.
 -k,--keystore <loc>    keystore path.
 -m,--merge <loc...>    path of .apatch files.
 -n,--name <name>       patch name.
 -o,--out <dir>         output dir.
 -p,--kpassword <***>   keystore password.

下面就開始使用這個命令來製作patch文件:

執行如下命令:
apkpatch.sh -f fixed.apk -t bug.apk -o out -k sig -p 123123 -a test_sig -e 123123

命令的輸出:
add modified Method:V  onCreate(Landroid/os/Bundle;)  in Class:Lcom/euler/andfix/MainActivity;


第三步:到此補丁文件製作好了,就可以通過網絡等渠道推送到手機上。

查看out目錄,一共有3個,會在後面進行解釋:


這裏的.patch文件就是我們需要的,把它push到手機:



重起剛纔的APP,觀看Log輸出時否改變了:


^_^,補丁成功了,而且這個文件也才3KB多點哦,非常適合在線修復bug~~~~

-rw-rw-r-- 1 yanchen yanchen 3468 12月 15 18:32 out/fixed-07d99a18833f092518fbb041c793e53b.apatch


AndFix的源碼分析

============================================================================================================================

Patch的製作流程現在知道了,下一步就準備進入Code層面的分析,探究它的內部實現原理。

============================================================================================================================


首先來看Patch製作原理:

製作patch會用到apkpatch這個工具,而它會調用apkpatch-1.0.3.jar這個文件。反編譯這個jar包,發現在製作patch時,它會進入ApkPatch的doPatch()這個函數完成的。


進入doPatch函數看看:



這個函數邏輯挺清晰的,一共就這4個步驟。

第一步是diff,它會比對兩個APK的差別,來看看它的代碼是如何實現的:


這個函數會提取fixed.apk與bug.apk中的classes.dex文件,然後通過2個for循環,來對比每個class文件中的字段與函數是否完全一致。


那麼字段和函數又是如何對比的呢?

我們來看看compareMethod()函數內部實現:


原來對比函數時會看兩個是否有函數實現,有的話就會看兩個函數的函數體是否一致,代碼如下:


如果不一致就會添加到一個叫info的HashSet變量中。

而比對字段主要看字段的初始值是否一樣,代碼如下:



總結:

通過上面的步驟就比對出了兩個APK中的classes.dex中差別,並且將有差別的文件保存在了info變量中。


接下來進入第二步,開始進行buildCode(),這步的目的是將info中保存的文件寫到.smali文件中,然後再打包成一個dex文件。


比如下面的是我的demo產生的smali文件:




在生成的smali函數中,它會添加一個自定義的Anotation,叫MethodReplace



到此,第二個步驟也就完成了,它一共產生2個文件:



接下來進入第三個步驟,調用build()函數,這步會將上一步產生的dex文件寫入到一個jar文件,並進行簽名,代碼如下:


PatchBuilder的代碼如下:


所以第三步完成後,會產生一個叫作diff.apatch的jar文件。

接下來進入最後一步,調用release()函數,也就是通過它產生最終的patch文件。

看看release()中都做了些什麼事情:


主要是將第三步生成的diff.apatch文件重命名爲name-md5-.patch格式的文件


到此,patch的製作原理也就告一段落了,主要就是提取兩個APK中的classes.dex,對比他們中的class文件是否有區別,將有區別的提取出來打包到.apatch文件中。

最終的產物如下:


============================================================================================================================

以上是Patch的製作流程分析,下一下我們再來看看客戶端是如何將這個patch文件打入自己的APK中的。

============================================================================================================================


要使用這套框架,客戶端只需在Application的onCreate()中加入如下的代碼:


那我們就來看看這幾行代碼到底會做什麼事情。

init()函數主要工作時載入files/apatch/目錄下的.apatch文件,代碼如下:


initPatchs()代碼如下:

它讀取mPathDir目錄下的所有.patch文件,並將起添加到一個叫mPatchs的HashSet變量中。

這是mPatchDir的初始值:


看來這個函數的目的就是在啓動的時候,讀取所有本地的patch文件。

讀取完畢後調用loadPatch()來進行運行時的APK修復,代碼如下:


fix()函數的代碼如下:


它會加載.apatch文件中class,然後再調用fixClass(),繼續往下看:


會讀取自定義的Anotation MethodReplace,通過它獲取到class名字與method名字,進行替換。

在前面的分析中也介紹過MethodReplace的內容格式,可參考如下:


而在進行Replace Method是,是在Native層做:


在JNI的代碼中,支持Dalvik與ART,這時它的代碼結構:


其修復原理就是從內存中找出原來函數指令指針,讓它指向新的函數地址:



上面的meth變量便是我們bug.apk中的函數的句柄,target便是.aptach文件中函數的句柄。

而insns是函數指令地址的指針,解釋如下:



到此函數替換的原理就水落石出了,就是函數Hook。

很犀利的做法。


【總結】

所以總結一下補丁執行的原理就是:

在運行時,讀取patch文件中的函數,將它的函數指令地址賦給APK中的函數。

這樣不就等於替換了原來的函數麼?那麼bug也就可以被消除了。。。

當然這一切都是在內存中進行的,不會對本地的APK有任何影響。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章