本项目已经上传至码云,有兴趣的哥们可以参考。
码云:https://gitee.com/xuzhaoyu/HotFixDemo.git
老规矩,先看效果图:
Gradle 3.X发布,Android8.0发布,兼容性始终是热修复需要考虑的重要问题。阿里巴巴AndFix停止更新2年多,腾讯Nuwa(女娲)也停更两年多,那么现在还有什么热修复框架可以选择呢?市面上热修复主要可以分为两类:一类是基于multidex的热更新框架,包括Nuwa、Tinker等;另一类就是native hook方案,如阿里的AndFix和Dexposed。但上述方案或多或少都有一些问题,基于native hook的方案:需要针对dalvik虚拟机和art虚拟机做适配,需要考虑指令集的兼容问题,需要native代码支持,兼容性上会有一定的影响;基于Multidex的方案,需要反射更改DexElements,改变Dex的加载顺序,这使得patch需要在下次启动时才能生效,实时性受到了影响,同时这种方案在android N [speed-profile]编译模式下可能会有问题。借鉴Android Studio的重要的新特性InstantRun(它实现了对代码修改的实时生效,即热插拔)原理,兼容性99.9%以上的Robust诞生了。
现在两行哥带你踩坑大名鼎鼎的Robust。
一、Demo体验
图中两个文件:app-release.apk和patch.jar。前者为带有Bug的demo,直接安装即可。后者为补丁包,通过服务器下发给用户。
比如,服务器提供下载地址,在用户打开app的时候后台静默下载补丁包并安装。
1.为了简便,两行哥没有给demo动态申请权限。6.0以后版本的机器安装后,请手动打开存储空间权限,如下图(MIUI):
2.在手机内部存储目录中建立文件夹robust,将patch.jar补丁包拷贝进去。准确的路径为:Environment.getExternalStorageDirectory().getPath() + File.separator + "robust" + File.separator + "patch.jar"。请确保补丁包路径正确。
3.开始体验Demo吧。
二、集成流程
1.在项目build.gradle添加依赖
classpath 'com.meituan.robust:gradle-plugin:0.4.71'
classpath 'com.meituan.robust:auto-patch-plugin:0.4.71'
2.在app build.gradle添加依赖:
apply plugin: 'auto-patch-plugin'
apply plugin: 'robust'
compile 'com.meituan.robust:robust:0.4.71'
3.添加robust.xml配置文件
坑1:forceInsert选项建议fasle。在AndroidStudio 3.x "com.android.tools.build:gradle:3.x"可能存在兼容问题,请自行尝试。
注意,原理上,Robust是为每个函数都插入了一段逻辑,为每个class插入了ChangeQuickRedirect的字段,所以最终肯定会增加apk的体积,并影响代码执行效率(尽管影响很小)。所以exceptPackname选项中应当尽可能添加不需要插入代码的包(不需要热修复的包),如下图所示:
建议反编译Apk后查看所有的包。推荐使用AntiDroid。
请额外留意patchPackname配置的参数,可以写自定义任意包名,后面将会用到。
4.复写PatchManipulate。
自定义子类继承PatchManipulate,并实现3个抽象方法。
public class PatchManipulateImp extends PatchManipulate {
@Override
protected List fetchPatchList(Context context) {
Patch patch = new Patch();
patch.setName("patchFile");
//we recommend LocalPath store the origin patch.jar which may be encrypted,while TempPath is the true runnable jar
//LocalPath是存储原始的补丁文件,这个文件应该是加密过的,TempPath是加密之后的,TempPath下的补丁加载完毕就删除,保证安全性
//这里面需要设置一些补丁的信息,主要是联网的获取的补丁信息。重要的如MD5,进行原始补丁文件的简单校验,以及补丁存储的位置,这边推荐把补丁的储存位置放置到应用的私有目录下,保证安全性
patch.setLocalPath(Environment.getExternalStorageDirectory().getPath() + File.separator + "robust" + File.separator + "patch");
//setPatchesInfoImplClassFullName 设置项各个App可以独立定制,需要确保的是setPatchesInfoImplClassFullName设置的包名是和xml配置项patchPackname保持一致,而且类名必须是:PatchesInfoImpl
//请注意这里的设置
patch.setPatchesInfoImplClassFullName("com.example.robust.patch.PatchesInfoImpl");
List patches = new ArrayList<>();
patches.add(patch);
return patches;
}
@Override
protected boolean verifyPatch(Context context, Patch patch) {
patch.setTempPath(Environment.getExternalStorageDirectory().getPath() + File.separator + "robust" + File.separator + "patch");
boolean exists = new File(patch.getLocalPath()).exists();
if (exists) {
FileUtil.copyFile(patch.getLocalPath(), patch.getTempPath());
}
return true;
}
@Override
protected boolean ensurePatchExist(Patch patch) {
return true;
}
}
坑2:patch.setPatchesInfoImplClassFullName("com.example.robust.patch.PatchesInfoImpl");这里setPatchesInfoImplClassFullName的参数必须是第3步中patchPackname配置的包名+PatchesInfoImpl。
坑3:这是最大的坑。类似ClassNotFoundException:Didn't find class "com.meituan.robust.patch.PatchesInfoImpl" on path: DexPathList[[],nativeLibraryDirectories=[/vendor/lib64, /system/lib64]] 。这个异常是因为补丁文件中“com.meituan.robust.patch.PatchesInfoImpl”不存在,或者补丁文件本身就不存在。可能你说,我已经给patch.setLocalPatch()设置过路径,并且查看了手机存储,在这个路径下确实存在patch.jar。其实Robust在应用补丁时,实际上真正读取的路径是patch.getTempPatch(),请设置patch.setTempPath()。可能你还说,我已经设置过了,可是还是报这个异常。我们看看源码中这个坑爹的patch.getLocalPath()到底返回了什么路径。
public String getTempPath() {
return tempPath + "_temp" + ".jar";
}
真相大白,原来返回的并不是tempPath,而是tempPath + "_temp" + ".jar"。也就是说,如果你设置的tempPatch为/storage/emulated/0/robust/patch,那么Robust在应用补丁时,它会去寻找/storage/emulated/0/robust/patch_temp.jar。请检查这个文件是否存在,不存在的话,拷贝或者剪切一份,IO流不用两行哥教吧。
5.在App打开的时候添加检查服务器是否存在热修复补丁的逻辑。
第一个参数为AppContext,第二个参数为第4步中自定义的子类,第三个参数为实现的回调。别忘了最后的start()开启子线程。
new PatchExecutor(getApplicationContext(), new PatchManipulateImp(), new RobustCallBack() {
@Override
public void onPatchListFetched(boolean result, boolean isNet, List patches) {
}
@Override
public void onPatchFetched(boolean result, boolean isNet, Patch patch) {
}
@Override
public void onPatchApplied(boolean result, Patch patch) {
}
@Override
public void logNotify(String log, String where) {
}
@Override
public void exceptionNotify(Throwable throwable, String where) {
}
}).start();
6.生成签名Apk。
请生成正式Apk,然后在app/build/outputs/robust的文件夹内会生成方法清单文件methodsMap.robust。在app文件夹内新建robust文件夹,将methodsMap.robust拷贝进去。methodsMap.robust请一定妥善保存,丢失将会无法生成补丁。如果开启了混淆,请参考文末附录其他文章,需要一些额外操作。
7.修改代码并生成补丁。
如果线上存在Bug,那么如何修改代码及生成补丁呢?
需要确保methodsMap.robust文件为上次生成正式Apk时的最新产生的版本。在要修改的方法上添加@Modify注解,在要添加的方法上添加@Add。
在app的build.gradle中打开auto-patch-plugin。重新编译项目,并生成签名后的Apk。生成Apk的参数和配置必须和第6步一样。
在生成Apk的过程中,会报异常Error:Execution failed for task ':app:transformClassesWithAutoPatchTransformForRelease'.> auto patch end successfully,不用紧张,其实已经生成了补丁包patch.jar,在app/build/outputs/robust/patch.jar。
坑4:如果生成补丁时的参数与第6步生成Apk的参数不一致,会导致补丁生成异常。
坑5:如果打包时没有正式签名,可能会在安装的时候提示Apk错误,请自测。
附录:其他参考资料
1.入门资料:
http://www.jcodecraeer.com/plus/view.php?aid=7797
2.技术分析
https://tech.meituan.com/android_robust.html
3.wiki和常见问题
https://github.com/Meituan-Dianping/Robust/wiki