Andfix和hotfix是两种android热修复框架。
android的热修复技术我看的最早的应该是QQ空间团队的解决方案,后来真正需要了,才仔细调查,现在的方案中,阿里有两种Dexposed和Andfix框架,由于前一种不支持5.0以上android系统,所以阿里系的方案我们就看Andfix就好。Hotfix框架算是对上文提到的QQ空间团队理论实现。本文旨在写实现方案,捎带原理。
框架官网: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,不能大规模做更改。如果我们觉得这种修复不能满足修复要求,那么,可以看另外这种,局限更少的热修方案。
官网: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代码