首页 > 程序开发 > 移动开发 > Android >

Android热修复:Andfix和Hotfix,两种方案的比较与实现

2016-10-09

android的热修复技术我看的最早的应该是QQ空间团队的解决方案,后来真正需要了,才仔细调查,现在的方案中,阿里有两种Dexposed和Andfix框架,由于前一种不支持5 0以上android系统

android的热修复技术我看的最早的应该是QQ空间团队的解决方案,后来真正需要了,才仔细调查,现在的方案中,阿里有两种Dexposed和Andfix框架,由于前一种不支持5.0以上android系统,所以阿里系的方案我们就看Andfix就好。Hotfix框架算是对上文提到的QQ空间团队理论实现。本文旨在写实现方案,捎带原理。

Andfix

引入

框架官网:https://github.com/alibaba/AndFix

使用android studio开发,引入如下:

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

原理

下面是个修复的过程图,供我们更好地理解。

技术分享

可以看出,andfix的修复是方法级的,对有bug的方法进行替换。

做补丁

官方有给使用方式,不过比较简略,所以会有些修改。我的思路是把补丁制作好,然后放到服务器上,客户端下载补丁到指定文件夹,然后修复。
首先要有补丁的制作工具,官方也为我们准备好了:这里
解压后,我们把修复前的apk和修复后的apk,keystore(为了方便,我就用debug的keystore了)放到这个文件夹里,如下:
技术分享
其中需要用命令做补丁文件,就是需要一个修复前的apk和修复后的apk做对比,命令含义如下:<喎"http://www.2cto.com/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;"> 命令 : 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[&lsquo;DOCUMENT_ROOT&lsquo;];//网站根目录的绝对地址
$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(&lsquo;processWithJavassist&lsquo;) << {
    String classPath = file(&lsquo;build/intermediates/classes/debug&lsquo;)//项目编译class所在目录
    dodola.patch.PatchClass.process(classPath, project(&lsquo;:hackdex&lsquo;).buildDir
            .absolutePath + &lsquo;/intermediates/classes/debug&lsquo;)//第二个参数是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是更好的选择。

相关文章
最新文章
热点推荐