項(xiàng)目以飛快的速度迭代,2周進(jìn)行一次迭代升級(jí)炎滞。每次開(kāi)發(fā)完功能跑通愕掏,在現(xiàn)有機(jī)型上測(cè)試沒(méi)問(wèn)題的話奄薇,就提交市場(chǎng)。
在以極快的速度迭代的時(shí)候胆描,避免不了出現(xiàn)各種問(wèn)題——傳說(shuō)中的bug
或瞬雹,重大bug悄晃,需要緊急修復(fù)
或训唱,可以下次迭代修復(fù)的bug
或褥蚯,影響用戶體驗(yàn)的行為
出現(xiàn)bug后,我們的一貫做法:
發(fā)布緊急版本况增,等待用戶下載更新赞庶。
可是,并不是所有的用戶都會(huì)下載更新澳骤。那該怎么辦歧强?
強(qiáng)制更新,這是不太友好的行為为肮,如果人現(xiàn)在用的是流量摊册,強(qiáng)制別人更新,會(huì)耗費(fèi)別人流量颊艳,還是挺貴的茅特!這確實(shí)有點(diǎn)說(shuō)不過(guò)去。
有沒(méi)有一種方法棋枕,在用戶不下載新App的前提下修復(fù)這些bug白修。當(dāng)然有,以下是我找到的兩種解決方案重斑。
有兩種解決方案:
##Dexposed
Dexposed是基于開(kāi)源Xposed框架實(shí)現(xiàn)的一個(gè)Android平臺(tái)上功能強(qiáng)大的無(wú)侵入式運(yùn)行時(shí)AOP框架兵睛。
Dexposed的AOP實(shí)現(xiàn)是完全無(wú)侵入式的,沒(méi)有使用任何注解處理器窥浪,編織器或者字節(jié)碼重寫(xiě)器祖很。只需要在應(yīng)用初始化階段加載一個(gè)很小的JNI庫(kù)就可以完成Dexposed框架的集成。
Dexposed不僅可以hook應(yīng)用中的自定義函數(shù)漾脂,也可以hook你的應(yīng)用進(jìn)程中調(diào)用的Android框架的函數(shù)突琳。這個(gè)特性對(duì)于Android開(kāi)發(fā)者來(lái)說(shuō)有很多好處,因?yàn)槲覀儑?yán)重依賴于Android SDK的版本碎片化符相。
借助動(dòng)態(tài)類加載技術(shù)拆融,運(yùn)行中的app可以加載一小段經(jīng)過(guò)編譯的Java AOP代碼,在不需要重啟app的前提下實(shí)現(xiàn)修改目標(biāo)app的行為啊终。
典型的應(yīng)用場(chǎng)景
AOP編程
插樁(例如測(cè)試镜豹,性能監(jiān)控等)
在線熱更新,修復(fù)嚴(yán)重的蓝牲,緊急的或者安全性的bug
SDK hooking以提供更好的開(kāi)發(fā)體驗(yàn)
在這里趟脂,我只關(guān)心Dexposed在Android平臺(tái)上的在線熱更新Hotfix
的使用:
在Android平臺(tái),Dexposed支持函數(shù)級(jí)別的在線熱更新例衍,例如對(duì)已經(jīng)發(fā)布在應(yīng)用市場(chǎng)上的APK昔期,當(dāng)我們從crash統(tǒng)計(jì)平臺(tái)上發(fā)現(xiàn)某個(gè)函數(shù)調(diào)用有bug已卸,導(dǎo)致經(jīng)常性crash,而這個(gè)時(shí)候離下次迭代的預(yù)定發(fā)布日期還有一段時(shí)間硼一。這種情況下累澡,可以在本地開(kāi)發(fā)一個(gè)補(bǔ)丁APK,并發(fā)布到服務(wù)器中般贼,應(yīng)用市場(chǎng)上的APK下載這個(gè)補(bǔ)丁APK并集成后愧哟,就可以修復(fù)這個(gè)crash。
上述涉及到兩個(gè)角色:
- 宿主apk——已經(jīng)發(fā)布在應(yīng)用市場(chǎng)上哼蛆,有bug并且需要修復(fù)的apk;
- 補(bǔ)丁apk——在本地開(kāi)發(fā)的蕊梧,修復(fù)了bug的apk;
HotFix是什么
HotFix用來(lái)修復(fù)線上嚴(yán)重的,緊急的或者安全性的bug腮介,這里會(huì)涉及到兩個(gè)apk文件:一個(gè)我們稱為宿主apk肥矢,也就是發(fā)布到應(yīng)用市場(chǎng)的apk,一個(gè)稱為補(bǔ)丁apk叠洗。宿主apk出現(xiàn)bug時(shí)甘改,通過(guò)在線下載的方式從服務(wù)器下載補(bǔ)丁apk,使用補(bǔ)丁apk中的函數(shù)替換原來(lái)的函數(shù)惕味,從而實(shí)現(xiàn)在線修復(fù)bug的功能楼誓。
如何借助Dexposed實(shí)現(xiàn)HotFix
- 需要再引入一個(gè)名為patchloader的jar包,這個(gè)函數(shù)庫(kù)實(shí)現(xiàn)了一個(gè)熱更新框架玉锌,宿主apk在發(fā)布時(shí)會(huì)將這個(gè)jar包一起打包進(jìn)apk中
- 補(bǔ)丁apk只是在編譯時(shí)需要這個(gè)jar包名挥,但打包成apk時(shí)不包含這個(gè)jar包,以免補(bǔ)丁apk集成到宿主apk中時(shí)發(fā)生沖突
- 補(bǔ)丁apk將會(huì)以provided的形式依賴dexposedbridge.jar和patchloader.jar,補(bǔ)丁apk的build.gradle文件中依賴部分腳本如下所示
dependencies {
provided files('libs/dexposedbridge.jar')
provided files('libs/patchloader.jar')
}
支持狀態(tài):
Dexposed支持從Android2.3到4.4(除了3.0)的所有dalvid運(yùn)行時(shí)arm架構(gòu)的設(shè)備主守,穩(wěn)定性已經(jīng)經(jīng)過(guò)實(shí)踐檢驗(yàn)禀倔。
缺點(diǎn):
我的設(shè)備是5.0 或者更高系統(tǒng)的使用起來(lái)就有點(diǎn)問(wèn)題了。
因此参淫,我并沒(méi)打算仔細(xì)的學(xué)習(xí)Dexposed救湖,只是做一個(gè)簡(jiǎn)要的了解。
具體使用可以參見(jiàn)這個(gè)博客涎才,寫(xiě)的很棒:
使用示例:Android平臺(tái)免Root無(wú)侵入AOP框架Dexposed使用詳解
##AndFix
AndFix is What ?
經(jīng)過(guò)綜合對(duì)比鞋既,發(fā)現(xiàn)AndFix可用性比較強(qiáng),因此決定學(xué)習(xí)AndFix的使用耍铜,從官方文檔入手邑闺。
AndFix是一個(gè)Android App的在線熱補(bǔ)丁框架。使用此框架棕兼,我們能夠在不重復(fù)發(fā)版的情況下陡舅,在線修改App中的Bug。AndFix就是 “Android Hot-Fix”的縮寫(xiě)伴挚。
它支持Android 2.3到6.0版本靶衍,并且支持arm與X86系統(tǒng)架構(gòu)的設(shè)備灾炭。完美支持Dalvik與ART的Runtime。AndFix 的補(bǔ)丁文件是以 .apatch 結(jié)尾的文件颅眶。它從你的服務(wù)器分發(fā)到你的客戶端來(lái)修復(fù)你App的bug 蜈出。
原理:
AndFix執(zhí)行的原理是實(shí)現(xiàn)方法體的替換,如下圖所示(該圖片來(lái)自AndFix github介紹):
方法替換:
AndFix通過(guò)Java的自定義注解來(lái)判斷一個(gè)方法是否應(yīng)該被替換帚呼,如果可以就會(huì)hook該方法并進(jìn)行替換掏缎。AndFix在ART架構(gòu)上的Native方法是art_replaceMethod
、在X86架構(gòu)上的Native方法是dalvik_replaceMethod
煤杀。他們的實(shí)現(xiàn)方式是不同的眷蜈。對(duì)于Dalvik,它將改變目標(biāo)方法的類型為Native
同時(shí)hook方法的實(shí)現(xiàn)至AndFix自己的Native
方法沈自,這個(gè)方法稱為 dalvik_dispatcher
,這個(gè)方法將會(huì)喚醒已經(jīng)注冊(cè)的回調(diào)酌儒,這就是我們通常說(shuō)的hooked
(掛鉤)。對(duì)于ART來(lái)說(shuō)枯途,我們僅僅改變目標(biāo)方法的屬性來(lái)替代它忌怎。
更多信息,見(jiàn)這里:AndFix/jni/
Fix 流程酪夷,大致如下:
- 發(fā)現(xiàn)bug(友盟榴啸、TestIn 、用戶反饋)
- 分析bug產(chǎn)生原因
- 創(chuàng)建并發(fā)布補(bǔ)丁
- App打補(bǔ)丁
在App中引入AndFix
如何來(lái)獲得AndFix 晚岭?
將AndFix aar 作為library編譯入你的工程:
- 如果你用的是maven依賴鸥印,添加如下代碼:
<dependency>
<groupId>com.alipay.euler</groupId>
<artifactId>andfix</artifactId>
<version>0.3.1</version>
<type>aar</type>
</dependency>
- 如果你用的是gradle依賴,添加如下代碼:
dependencies {
compile 'com.alipay.euler:andfix:0.3.1@aar'
}
如何使用AndFix 坦报?
- 初始化 PatchManager
patchManager = new PatchManager(context);
patchManager.init(appversion);//current version
- Load patch 加載補(bǔ)丁
patchManager.loadPatch();
你應(yīng)該盡可能早的加載補(bǔ)丁库说,通常都是在Application的onCreate()方法中進(jìn)行初始化。
- Add patch 添加補(bǔ)丁
patchManager.addPatch(path);//path:補(bǔ)丁文件下載到本地的路徑片择。
當(dāng)一個(gè)新的補(bǔ)丁文件被下載后潜的,調(diào)用addPatch(path)就會(huì)立即生效。
開(kāi)發(fā)工具
AndFix 提供了一個(gè)補(bǔ)丁創(chuàng)建工具 apkpatch.
如何使用這個(gè)工具
1.準(zhǔn)備兩個(gè)應(yīng)用apk: 一個(gè)是線上的apk字管,另一個(gè)是修復(fù)了bug的apk.
2.通過(guò)提供的兩個(gè)apk生成補(bǔ)丁文件.
運(yùn)行如下命令:
usage: apkpatch -f <new> -t <old> -o <output> -k <keystore> -p <***> -a <alias> -e <***>
-a,--alias <alias> keystore entry alias.
-e,--epassword <***> keystore 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.
這個(gè)時(shí)候啰挪,你就有了你的應(yīng)用的救世主——補(bǔ)丁文件。接下來(lái)你就可以通過(guò)某種方式將該補(bǔ)丁文件分發(fā)給你的客戶端嘲叔。
- 或亡呵,adb push 到sdk
- 或,發(fā)布到服務(wù)器借跪,客戶端下載該apatch
有時(shí)候政己,你的團(tuán)隊(duì)成員可能會(huì)fix各自的bug,這個(gè)時(shí)候就會(huì)有不止一個(gè)補(bǔ)丁文件.apatch
。這種情況下,你可以用這個(gè)工具merge這些.apatch
文件:
usage: apkpatch -m <apatch_path...> -o <output> -k <keystore> -p <***> -a <alias> -e <***>
-a,--alias <alias> keystore entry alias.
-e,--epassword <***> keystore 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.
運(yùn)行示例
- 導(dǎo)入samplesI/AndFixDemo進(jìn)你的IDE歇由,給AndFixDemo添加AndFix(library project 或者 aar)的依賴卵牍。
- build 工程,保存應(yīng)用為1.apk沦泌,然后將該apk安裝到手機(jī)或者模擬器上糊昙。
- 修改com.euler.test.A,作為Fix后的包谢谦。
- build 工程释牺,保存應(yīng)用為2.apk
- Use apkpatch tool to make a patch. 使用apkpatch 工具制作一個(gè)補(bǔ)丁文件。
- Rename the patch file to out.apatch, and then copy it to sdcard.重命名補(bǔ)丁文件為out.apatch 回挽,然后將它拷貝到sdcard上没咙。
- 運(yùn)行1.apk然后查看log
注意
混淆
如果你使用混淆,你要保存mapping.txt千劈,這樣的話你在新版本的構(gòu)建是就可以借助 “"-applymapping” 來(lái)使用它了祭刚。
混淆時(shí)需要保留下面的內(nèi)容:
- Native方法 ,比如:
com.alipay.euler.andfix.AndFix
- Annotation墙牌,比如:
com.alipay.euler.andfix.annotation.MethodReplace
為了確保這些類在運(yùn)行了ProGuard后涡驮,可以被找見(jiàn),在ProGuard配置中添加如下代碼:
-keep class * extends java.lang.annotation.Annotation
-keepclasseswithmembernames class * {
native <methods>;
}
局限性
如果你使用了喜滨,軟件加固比如 Bangcle捉捅,為了生成補(bǔ)丁文件,你最好使用為經(jīng)過(guò)加固的apk.使用這些加固虽风,很可能使熱補(bǔ)丁失效棒口。
安全性
驗(yàn)證apatch文件的簽名是否就是在使用apkpatch工具時(shí)使用的簽名(如果不驗(yàn)證那么任何人都可以制作自己的apatch文件來(lái)對(duì)你的APP進(jìn)行修改)
驗(yàn)證optimize file的指紋(防止有人替換掉本地保存的補(bǔ)丁文件,所以要驗(yàn)證MD5碼)
以下是我在用demo時(shí)碰到的問(wèn)題:
- 運(yùn)行apkpatch報(bào)nullPointException
關(guān)于該問(wèn)題的github issue : 運(yùn)行apkpatch報(bào)nullPointException
解決方法:
運(yùn)行命令行時(shí) alias 填錯(cuò)了焰情,應(yīng)該填alias名字陌凳,我填成了alias路徑剥懒。修改后内舟,解決了問(wèn)題。
zhanggeng:apkpatch-1.0.3$
./apkpatch.sh
-f /Users/zhanggeng/Desktop/after/app-release.apk
-t /Users/zhanggeng/Desktop/before/app-release.apk
-o /Users/zhanggeng/Desktop/apatch/
-k /Users/zhanggeng/Desktop/fixdemo.jks
-p 123456
-a fixdemo
-e 123456
- 運(yùn)行程序初橘,檢測(cè)是否可以使用時(shí)验游,拋出異常:java.util.zip.ZipException: File too short to be a zip file: 0
關(guān)于該問(wèn)題的github issue: java.util.zip.ZipException: File too short to be a zip file: 0
解決方法:
這個(gè)問(wèn)題是,上面那個(gè)問(wèn)題造成的保檐。只要補(bǔ)丁文件生成正確耕蝉,是不會(huì)有這個(gè)問(wèn)題的。
參考文章:
Demo下載地址:AndFixDemo