Android熱修復:Andfix和Hotfix,兩種方案的比較與實現
標籤:
Andfix和hotfix是兩種android熱修復框架。
android的熱修復技術我看的最早的應該是QQ空間團隊的解決方案,後來真正需要了,才仔細調查,現在的方案中,阿里有兩種Dexposed和Andfix框架,由於前一種不支持5.0以上android系統,所以阿里系的方案我們就看Andfix就好。Hotfix框架算是對上文提到的QQ空間團隊理論實現。本文旨在寫實現方案,捎帶原理。
Andfix
引入
框架官網:https://github.com/alibaba/AndFix
介紹是用英文寫的,所以附上翻譯網址:
http://blog.csdn.net/qxs965266509/article/details/49802429
使用android studio開發,引入如下:
compile ‘com.alipay.euler:andfix:0.4.0@aar‘
原理
下面是個修復的過程圖,供我們更好地理解。
可以看出,andfix的修復是方法級的,對有bug的方法進行替換。
做補丁
官方有給使用方式,不過比較簡略,所以會有些修改。我的思路是把補丁製作好,然後放到服務器上,客戶端下載補丁到指定文件夾,然後修復。
首先要有補丁的製作工具,官方也爲我們準備好了:這裏
解壓後,我們把修復前的apk和修復後的apk,keystore(爲了方便,我就用debug的keystore了)放到這個文件夾裏,如下:
其中需要用命令做補丁文件,就是需要一個修復前的apk和修復後的apk做對比,命令含義如下:
命令 : apkpatch.bat -f new.apk -t old.apk -o output1 -k debug.keystore -p android -a androiddebugkey -e android
-f <new.apk> :新版本
-t <old.apk> : 舊版本
-o <output> : 輸出目錄
-k <keystore>: 打包所用的keystore
-p <password>: keystore的密碼
-a <alias>: keystore 用戶別名
-e <alias password>: keystore 用戶別名密碼
然後會在outputdic裏生成一個後綴是.apatch的文件:
改名成out.apatch,這就是我們的補丁。
打補丁
如何使用補丁呢?和把大象裝進冰箱是一樣步驟。
下面直接上代碼了:
第一步:把補丁放到服務器。
簡單起見,用的xampp,寫了段php代碼,起到下載的功能就可以了。
<?php
$file_name = "out.apatch";//需要下載的文件
define("SPATH","/files/");//存放文件的相對路徑
$file_sub_path = $_SERVER[‘DOCUMENT_ROOT‘];//網站根目錄的絕對地址
$file_path = $file_sub_path.SPATH.$file_name;//文件絕對地址,即前面三個連接
//判斷文件是否存在
if(!file_exists($file_path)){
echo "該文件不存在";
return;
}
$fp = fopen($file_path,"r");//打開文件
$file_size = filesize($file_path);//獲取文件大小
/*
*下載文件需要用到的header
*/
header("Content-type:application/octet-stream");
header("Accept-Ranges:bytes");
header("Accept-Length:".$file_size);
header("Content-Disposition:attachment;filename=".$file_name);
$buffer=1024;
$file_count=0;
//向瀏覽器返回數據
while(!feof($fp) && $file_count<$file_size){
$file_con = fread($fp,$buffer);
$file_count += $buffer;
echo $file_con;//這裏如果不echo,只會下載到0字節的文件
}
fclose($fp);
?>
第二步:下載和打補丁。
回到android,在我們的application裏:
public class App extends Application {
@Override
public void onCreate() {
super.onCreate();
YuanAndfix.inject(this);
}
}
其中,YuanAndfix類:
public class YuanAndfix {
public static final String apatch_path = "out.apatch";
public static void inject(final Context context) {
final PatchManager patchManager = new PatchManager(context);
patchManager.init(BuildConfig.VERSION_CODE + "");//current version
patchManager.loadPatch();
new Thread(new Runnable() {
@Override
public void run() {
HttpDownload httpDownload = new HttpDownload();
httpDownload.downFile("http://192.168.1.12/download.php", context.getDir("patch", Context.MODE_PRIVATE).getAbsolutePath()+"/",apatch_path);
try {
String patchPath =context.getDir("patch", Context.MODE_PRIVATE).getAbsolutePath()+"/"+apatch_path;
File file = new File(patchPath);
if (file.exists()) {
patchManager.addPatch(patchPath);
Toast.makeText(context,"打補丁完成",Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(context,"失敗",Toast.LENGTH_SHORT).show();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
}
這樣,熱修復就完成了,我這個例子是點擊按鈕,彈出toast顯示文字,修復前是
Toast.makeText(MainActivity.this,"bug",Toast.LENGTH_SHORT).show();
修復後是:
Toast.makeText(MainActivity.this,"fixed",Toast.LENGTH_SHORT).show();
以上就是Andfix的使用,經過我的試驗,使用這個框架的侷限在於不能修改全局變量,不能加新的方法,不過可以在現有的方法上做修改,加局部變量。從這方面來看,Andfix其實要求我們只是修改方法裏面的bug,不能大規模做更改。如果我們覺得這種修復不能滿足修復要求,那麼,可以看另外這種,侷限更少的熱修方案。
HotFix
原理
官網:https://github.com/dodola/HotFix
在用這個框架之前,我希望你先去看一下原理,對後面的實現有很大幫助。
下面我簡單說一下原理。
把多個dex文件塞入到app的classloader之中android加載的時候,如果有多個dex文件中有相同的類,就會加載前面的類,所以這個熱補的原理就是把有問題的類替換掉,把需要的類放到最前面,達到熱補的目的。
但是有個問題,我們想要替換的類,不能被打上CLASS_ISPREVERIFIED標誌,否則回報錯,於是這個方案的難點就在於如何讓想要被修復的類不被打上CLASS_ISPREVERIFIED標誌。所以,大神們的hack神計來了,先製作一個dex包,然後給我們想要修復的類的構造方法,都注入這個dex包,其實就是輸出這個dex包的一個類:
System.out.println(dodola.hackdex.AntilazyLoad.class);
這樣,就可以讓我們想要修復的類不被打上CLASS_ISPREVERIFIED標誌,然後就可以加載補丁了。
框架
這個框架的使用不管是配置上,還是補丁生成上,都相對麻煩一些,雖然有個相似的框架Nuwa做了自動化這塊,不過據說有些坑沒人填,所以果斷用這個hotfix框架。框架下載下來,我們先看一下結構。
app是主工程;
buildSrc是Gradle的Task,Gradle的編譯命令就是由多個task組成的,說白了就是Gradle在編譯程序的時候會按照這些task順序執行命令。
hackdex裏面就一個空類,目的爲了讓編譯通過,讓主工程的類不被打上CLASS_ISPREVERIFIED標誌。
hotfixlib是個修復的工具類。
接着,我們看一下他們是怎麼一起工作的。
首先是主工程app的build.gradle文件,裏面多了兩段代碼:
task(‘processWithJavassist‘) << {
String classPath = file(‘build/intermediates/classes/debug‘)//項目編譯class所在目錄
dodola.patch.PatchClass.process(classPath, project(‘:hackdex‘).buildDir
.absolutePath + ‘/intermediates/classes/debug‘)//第二個參數是hackdex的class所在目錄
}
和
applicationVariants.all { variant ->
variant.dex.dependsOn << processWithJavassist //在執行dx命令之前將代碼打入到class中
}
這就是通過javassist,給主工程的類的構造方法注入
System.out.println(dodola.hackdex.AntilazyLoad.class);
AntilazyLoad.class在app的assets中,程序運行後會拷貝到sd卡里,主要是爲了讓主工程的類不被打上CLASS_ISPREVERIFIED標誌。
做補丁
補丁就是想要替換的類的class文件的集合,補丁製作過程參考
https://github.com/dodola/HotFix;
其中用到的類在這裏提前:
接着把修復好的類放到一個文件夾,文件夾路徑得和你原來類的包名一致。如:
比如上圖的BugClass.class類,就放到這樣的文件夾
然後執行命令:
這樣就生成了一個path.jar在d盤下,接着就是把這個jar做成dex的jar了,由於要用到dx,而這個dx在我們的sdk工具包裏,所以我把這個path.jar拷貝到sdk工具包,利用dx命令
然後會生成path_dex.jar,這就是我們的補丁文件了。
打補丁
public class HotfixApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
File dexPath = new File(getDir("dex", Context.MODE_PRIVATE), "hackdex_dex.jar");
Utils.prepareDex(this.getApplicationContext(), dexPath, "hackdex_dex.jar");
HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.AntilazyLoad");
try {
this.getClassLoader().loadClass("dodola.hackdex.AntilazyLoad");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
然後是下載和打補丁
switch (item.getItemId()) {
case R.id.action_fix: {
new Thread(new Runnable() {
@Override
public void run() {
String url = "http://192.168.1.12/download.php";
HttpDownload httpDownload = new HttpDownload();
final int flag = httpDownload.downFile(url, MainActivity.this.getDir("dex", Context.MODE_PRIVATE).getAbsolutePath()+"/", "path_dex.jar");
HotFix.patch(MainActivity.this, MainActivity.this.getDir("dex", Context.MODE_PRIVATE).getAbsolutePath()+"/"+"path_dex.jar", "");
runOnUiThread(new Runnable() {
@Override
public void run() {
String fileState=null;
if (flag==0) {
fileState = "下載完成";
} ;
if (flag==1) {
fileState = "文件已存在";
}
if (flag==-1) {
fileState = "下載錯誤";
}
Toast.makeText(MainActivity.this, fileState, Toast.LENGTH_SHORT).show();
}
});
}
}).start();
}
break;
case R.id.action_test:
LoadBugClass bugClass = new LoadBugClass();
Toast.makeText(this, "測試調用方法:" + bugClass.getBugString(), Toast.LENGTH_SHORT).show();
break;
}
這裏需要注意,如果類一旦調用過,需要下次啓動程序補丁纔會生效。所以如果我們先點了測試,再點下載,那麼需要重啓程序(後臺殺死),補丁纔會生效。
手動注入
上面關於防止類被打上CLASS_ISPREVERIFIED標誌的辦法雖然好,但是是有侷限性的,必須要用gradle編譯,還得了解字節碼注入,如果我們是用eclipse開發,那就不能用了,其實我們還有一種辦法,就是手動給類添加那行
System.out.println(dodola.hackdex.AntilazyLoad.class)代碼,只要保證編譯通過就可以了。所以這裏這麼辦,我們新建一個工程,androidstudio的話,
看main下,我們新建了個hack文件夾,裏面放了個hack.jar,裏面只有這麼個類:
public class AntilazyLoad {
}
然後,在我們主工程app裏面的類的構造方法,加入
System.out.println(dodola.hackdex.AntilazyLoad.class),這行代碼,就達到了手動注入的目的,就不需要那些複雜的task代碼,字節碼注入等操作。所以如果你是用eclipse的話,目錄就是這樣
這個jar包不會被打包進app,就是讓編譯通過,真正的AntilazyLoad.class其實還是在項目的assets包下的hack_dex.jar。
上述方法都是親測完全可行的,特別是這種手動注入的方法,能解決大部分開發者不會用熱更的困擾。這個辦法我是看這篇文章學到的。
PS:
1、這個框架不能修改用final修飾過得東西,切記。
2、官網給出的打補丁代碼
HotFix.patch(this, dexPath.getAbsolutePath(), "dodola.hackdex.BugClass");
這麼看的話,很不合理,第三個參數居然要傳bug類名,我們又不能預知哪個類會發生bug,所以我改成這樣
HotFix.patch(this, dexPath.getAbsolutePath(), "");
第三個參數不要了,親測,也是好使的。
總結
對比兩種解決方案,阿里的andfix更注重於改細節的bug,雖然它是從native層做得操作,但是框架封裝的很好,我們使用起來很簡便,而且有更新維護,據說阿里系的app打算都用這個。如果我們僅僅就是開發一款app,還沒有大改動,不會熱更全局變量,不會增加方法,那麼這個框架就是首選。
但是有的時候我們可能開發的是一款sdk,譬如友盟sdk之類,或者想熱更全局變量,增加方法,那麼andfix可以說是用不到的,所以這個時候hotfix是更好的選擇。
下載點這裏
Andfixdemo
HotFixdemo
服務端PHP代碼
Android熱修復:Andfix和Hotfix,兩種方案的比較與實現
標籤: