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

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

時間:2016-05-12 13:33:44      閱讀:2614      評論:0      收藏:0      [點我收藏+]

標籤:

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,兩種方案的比較與實現

標籤:

發佈了67 篇原創文章 · 獲贊 10 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章