調研背景:
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需要修改所有class的class_ispreverified標誌位,導致運行時性能有所損失)
|
1.跳過了類初始化,對於靜態或者構造函數或者class.forname()的處理可能會有問題 |
在線修復bug |
【實現原理】 阿里的開源項目,原理是函數Hook。GitHub上有介紹。 【源碼下載】 |
Nuwa (HotFix/DroidFix)
|
1.兼容性比AndFix好(Multi dex方案,沒有static的問題) 2.支持ART與Dalvik 3.支持6.0
|
1.編譯完成java代碼後,需要遍歷修改class文件,插入代碼,防止class被打上class_ispreverified標誌,這會導致運行性能降低 |
|
【實現原理】 Multidex的動態加載原理,參考:http://bugly.qq.com/blog/?p=781 |
結合我的目標,目標鎖定在第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中的。
============================================================================================================================
那我們就來看看這幾行代碼到底會做什麼事情。
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有任何影響。