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