Android热修复之AndFix
最近我看到身边的小伙伴在看android热修复相关的文章,正好趁着休息的时间我在掘金社区看到了一篇讲android热修复的文章,于是我仔细读完了那篇文章。在读完那边文章之后,我觉得写一个实现热修复的测试demo很简单呀!于是我又多找了几篇文章度了一下,加深了一下印象,我看到身边的小伙伴被一个cmd命令折磨得怒抓头皮的样子,于是决定自己也来写一个demo测试玩玩,也就有了下面的这篇文章。
我使用的是阿里团队做的一套热修复框架AndFix,在这里我就不讲热修复到底是怎么实现的,也不探讨QQ空间团队给出的热修复解决方案。
AndFix框架地址:https://github.com/alibaba/AndFix
接下来,我就讲讲我的尝试过程。
一、 创建新的android应用,我使用的是AndroidStudio,然后将框架配置到应用之中,配置方式有两种:
1、 通过build.gradle里导入andfix
compile 'com.alipay.euler:andfix:0.3.1'
2、 通过module的方式添加andfix
此处我使用的是第二种方法,因为可以直接查看和编辑源码,我也在一些博客中看到使用gradle有一个bug,下面再细讲这个bug以及怎么处理这个问题。
二、 建议使用导入module的方式,我们需要在app所在module中创建jniLibs文件夹,然后将armeabi和x86两个文件夹拷贝到刚刚创建的文件夹中,目录结构如下图所示:
当然从github上下载的andfix文件中的tools,doc等文件可以不用导入。
三、 我们需要自定义Application,在自定义的Application中我们需要添加对补丁文件的加载实现,源码如下:
public class MyApplication extends Application{
private static final String TAG = "MyApplication";
/**
* apatch文件
*/
private static final String APATCH_PATH= "/Out.apatch";
private PatchManagermPatchManager;
@Override
public void onCreate(){
super.onCreate();
// 初始化
mPatchManager = new PatchManager(this);
mPatchManager.init("1.0"); // 版本号
// 加载 apatch
mPatchManager.loadPatch();
//apatch文件的目录
StringpatchFileString = Environment.getExternalStorageDirectory().getAbsolutePath()+ APATCH_PATH;
File apatchPath = new File(patchFileString);
if (apatchPath.exists()){
Log.e(TAG, "补丁文件存在");
Toast.makeText(getApplicationContext(),"补丁文件存在", Toast.LENGTH_SHORT).show();
try {
//添加apatch文件
mPatchManager.addPatch(patchFileString);
} catch (IOExceptione) {
Log.e(TAG, "打补丁出错了");
error =e.toString();
Toast.makeText(getApplicationContext(),"打补丁出错了"+e.toString(), Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
} else {
Log.e(TAG, "补丁文件不存在");
Toast.makeText(getApplicationContext(),"补丁文件不存在", Toast.LENGTH_SHORT).show();
}
}
}
四、 接下来,我们需要模拟一下应用出bug的场景,我们就以简单的TextView提示内容来当作应用的出bug和正常运行的情况,源码如下所示:
public class MainActivity extends AppCompatActivity { private Button button, patchBtn; private TextView textTV; private MyApplication app; private static final String APATCH_PATH = "Out.apatch"; private final String url = "http://192.168.31.163/Out.apatch"; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); app = (MyApplication) getApplication(); button = (Button) findViewById(R.id.button); patchBtn = (Button) findViewById(R.id.getpathc); textTV = (TextView) findViewById(R.id.text); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // textTV.setText("APP出bug啦!"); textTV.setText("已经修复bug啦!"); } }); patchBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { downloadPatch(url); } }); } private void downloadPatch(final String url) { Toast.makeText(MainActivity.this, "开始下载", Toast.LENGTH_SHORT).show(); new Thread() { @Override public void run() { HttpClient client = new DefaultHttpClient(); HttpGet get = new HttpGet(url); HttpResponse response; try { response = client.execute(get); HttpEntity entity = response.getEntity(); int fileSize = Integer.parseInt(entity.getContentLength() + ""); int downloadSize = 0; InputStream is = entity.getContent(); FileOutputStream fos = null; if (is != null) { File file = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), APATCH_PATH); fos = new FileOutputStream(file); byte[] buf = new byte[1024]; int ch = -1; while ((ch = is.read(buf)) != -1) { fos.write(buf, 0, ch); downloadSize += ch; } fos.flush(); if (fos != null) { if (fileSize == downloadSize) { sendMsg(0); } fos.close(); } } } catch (Exception e) { e.printStackTrace(); } } }.start(); } private Handler update_handler = new Handler() { @Override public void handleMessage(Message msg) { if (!Thread.currentThread().isInterrupted()) { switch (msg.what) { case 0: //设置进度条最大值 Toast.makeText(MainActivity.this, "补丁下载完成", Toast.LENGTH_SHORT).show(); break; default: break; } } super.handleMessage(msg); } }; private void sendMsg(int flag) { Message msg = new Message(); msg.what = flag; update_handler.sendMessage(msg); } }
小伙伴们一眼就能看出来,我是通过TextView提示的内容来判断应用有没有bug。其中的downloadPatch()方法就是我用来实现从服务器下载补丁包到手机sdcard上的方法,如果小伙伴要运行就需要自己搭建一个简单的服务器,功能不用多复杂只要能成功下载文件就ok!这里我就不讲怎么搭建服务器,网上教程多得是,小伙伴动动手指就能找到很多教程。
五、 前面我们说了在gradle里导入andfix会有个问题,是在原来的项目中,加载一次补丁后,out.apatch文件会copy到getFilesDir目录下的/apatch文件夹中,在下次补丁更新时,会检测补丁是否已经添加在apatch文件夹下,已存在就不会复制加载sdcard的out.apatch,所以我们需要对框架中patch文件下的PatchManager类中的addPatch()方法进行修改。原来的方法如下:
public void addPatch(String path) throws IOException { File src = new File(path); File dest = new File(mPatchDir, src.getName()); if(!src.exists()){ throw new FileNotFoundException(path); } if (dest.exists()) { Log.d(TAG, "patch [" + path + "] has be loaded."); return; } FileUtil.copyFile(src, dest);// copy to patch's directory Patch patch = addPatch(dest); if (patch != null) { loadPatch(patch); } }
修改后,判断apatch下的out.apatch存在即删除掉,重新复制加载sdcard下的out.apatch。
public void addPatch(String path) throws IOException { File src = new File(path); File dest = new File(mPatchDir, src.getName()); if (!src.exists()) { throw new FileNotFoundException(path); } if (dest.exists()) { Log.d(TAG, "patch [" + src.getName() + "] has be loaded."); boolean deleteResult = dest.delete(); if (deleteResult) Log.e(TAG, "patch [" + dest.getPath() + "] has be delete."); else { Log.e(TAG, "patch [" + dest.getPath() + "] delete error"); return; } } FileUtil.copyFile(src, dest);// copy to patch's directory Patch patch = addPatch(dest); if (patch != null) { loadPatch(patch); } }
六、 我们需要将应用打包,需要对包进行签名,同时这里需要两个应用包,一个是有bug的应用包,一个是修复之后的应用包。然后,我们需要用到apkpatch这个工具,这个工具在框架的github地址里面有。
apkpatch.sh是在Linux下使用的脚本文件,apkpatch.bat是在window的命令行中使用的批处理文件。
七、 准备好两个安装包和apkpatch工具,将两个安装包都放到apkpatch这个文件夹里面,方便我们使用命令生成补丁包,我使用的是mac,所以使用apkpatch.sh来生成补丁包,完整命令如下:
./apkpatch.sh -f [新包]–t [旧包] –o [输出文件名称]–k [签名文件] –p [签名文件密码]–a [签名文件别名] –e [别名密码]
例如:
./apkpatch.sh -f NoBug.apk -t Bug.apk -o Out-k /Users/cyril/Desktop/key/andfix.jks -p cyril998 -a andfix -e cyril998
当你的命令行出现如下截图的内容,并且在apkpatch文件夹生成如下的文件夹,就表明补丁文件生成成功。
我们需要将.apatch文件改名为Out.apatch
八、 不要忘了把补丁文件放到服务器的下载地址里面去。接着我们在手机上安装有bug的应用,运行截图如下:
看到了吗?程序已经出错了,然后我们点击“下载补丁”按钮,从实现写定的服务端下载补丁包,出现如下截图之后,我们就可以将应用完全退出,最好是清一下后台。
为了确保下载成功,我们进入应用的内存空间去看看,有没有我们下载的补丁包,如下图所示:
上图表明我们已经下载成功了。接下来,我们退出应用并清空后台,然后再次打开应用,点击“运行异常”按钮,运行结果如下图所示:
上图告诉我们我们的热修复实验成功了!
九、 一些不细心的小伙伴会发现怎么我点击“下载补丁”并没有看到下载成功的Toast呢?怎么我讲补丁包手动放到内存里面还是提示”出bug啦”呢?
1、不要忘了涉及到网络请求、内存读写需要设置权限的,因为小编就是从这个坑爬起来的,我当时忘了加权限,反复测试了很久,最后突然想起来我没有添加权限,在AndroidManifest.xml文件中添加如下权限
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.INTERNET" />
2、因为需要使用HttpClient,所以需要导入相应的jar包,需要在app的lib中导入如下的jar包,
同时还需要在build.gradle中配置
compile files('libs/org.apache.http.legacy.jar')
十、 总结
1、 andfix只能对一些逻辑进行修复,不能修改资源和布局文件;
2、 我测试过加固的情况,加固不应用热修复的使用,但是有一点用来生成.apatch包的两个安装包都不能加壳,也就是在加壳之前生成,如果应用出了问题需要打补丁,就必须找到有问题并且未加固的安装包。另外,在加固前制作的补丁可以很容易的被反编译出源码,也就说增加了新的安全性问题。
3、 我使用了4.4.4系统的手机测试是没有问题的,但是5.0以上的手机没有一次成功过,我使用了一部5.1和一部6.0系统的手机全都crash掉了,错误如下:
java.lang.UnsatisfiedLinkError:No implementation found for booleancom.alipay.euler.andfix.AndFix.setup(boolean, int)
(triedJava_com_alipay_euler_andfix_AndFix_setup andJava_com_alipay_euler_andfix_AndFix_setup__ZI)。
其实框架中.so文件只提供了两个版本,并没有提供适用于arm64-v8a和armebia-v7的.so文件,所以在这些类型的cpu下当然找不到相关的方法。