Android熱修復:Andfix和Hotfix,兩種方案的比較與實現

Andfix和hotfix是兩種android熱修復框架。php

android的熱修復技術我看的最先的應該是QQ空間團隊的解決方案,後來真正須要了,才仔細調查,如今的方案中,阿里有兩種Dexposed和Andfix框架,因爲前一種不支持5.0以上android系統,因此阿里系的方案咱們就看Andfix就好。Hotfix框架算是對上文提到的QQ空間團隊理論實現。本文旨在寫實現方案,捎帶原理。java

Andfixandroid

引入git

框架官網:https://github.com/alibaba/AndFix 
介紹是用英文寫的,因此附上翻譯網址: 
http://blog.csdn.net/qxs965266509/article/details/49802429github

使用android studio開發,引入以下:瀏覽器

compile 'com.alipay.euler:andfix:0.4.0@aar'

原理服務器

下面是個修復的過程圖,供咱們更好地理解。app

能夠看出,andfix的修復是方法級的,對有bug的方法進行替換。框架

作補丁eclipse

官方有給使用方式,不過比較簡略,因此會有些修改。個人思路是把補丁製做好,而後放到服務器上,客戶端下載補丁到指定文件夾,而後修復。 
首先要有補丁的製做工具,官方也爲咱們準備好了:這裏 
解壓後,咱們把修復前的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: 
一、這個框架不能修改用final修飾過得東西,切記。 
二、官網給出的打補丁代碼

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代碼

相關文章
相關標籤/搜索