〔两行哥〕带你踩坑Robust热修复

本项目已经上传至码云,有兴趣的哥们可以参考。

码云:https://gitee.com/xuzhaoyu/HotFixDemo.git

老规矩,先看效果图:


〔两行哥〕带你踩坑Robust热修复_第1张图片
图1 Robust效果图

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体验

〔两行哥〕带你踩坑Robust热修复_第2张图片
图2 示例Demo位置

图中两个文件:app-release.apk和patch.jar。前者为带有Bug的demo,直接安装即可。后者为补丁包,通过服务器下发给用户。
比如,服务器提供下载地址,在用户打开app的时候后台静默下载补丁包并安装。

1.为了简便,两行哥没有给demo动态申请权限。6.0以后版本的机器安装后,请手动打开存储空间权限,如下图(MIUI):

〔两行哥〕带你踩坑Robust热修复_第3张图片
图3 手动打开读写手机存储权限

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'
〔两行哥〕带你踩坑Robust热修复_第4张图片
图4 项目build.gradle

2.在app build.gradle添加依赖:

apply plugin: 'auto-patch-plugin'
apply plugin: 'robust'

compile 'com.meituan.robust:robust:0.4.71'
〔两行哥〕带你踩坑Robust热修复_第5张图片
图5 app build.gradle

3.添加robust.xml配置文件

坑1:forceInsert选项建议fasle。在AndroidStudio 3.x "com.android.tools.build:gradle:3.x"可能存在兼容问题,请自行尝试。

〔两行哥〕带你踩坑Robust热修复_第6张图片
图6 添加配置文件

注意,原理上,Robust是为每个函数都插入了一段逻辑,为每个class插入了ChangeQuickRedirect的字段,所以最终肯定会增加apk的体积,并影响代码执行效率(尽管影响很小)。所以exceptPackname选项中应当尽可能添加不需要插入代码的包(不需要热修复的包),如下图所示:


〔两行哥〕带你踩坑Robust热修复_第7张图片
图7 添加排除包

建议反编译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请一定妥善保存,丢失将会无法生成补丁。如果开启了混淆,请参考文末附录其他文章,需要一些额外操作。


〔两行哥〕带你踩坑Robust热修复_第8张图片
图8 生成Apk

7.修改代码并生成补丁。

如果线上存在Bug,那么如何修改代码及生成补丁呢?
需要确保methodsMap.robust文件为上次生成正式Apk时的最新产生的版本。在要修改的方法上添加@Modify注解,在要添加的方法上添加@Add。


〔两行哥〕带你踩坑Robust热修复_第9张图片
图9 修改代码

在app的build.gradle中打开auto-patch-plugin。重新编译项目,并生成签名后的Apk。生成Apk的参数和配置必须和第6步一样。


〔两行哥〕带你踩坑Robust热修复_第10张图片
图10 修改app build.gradle

在生成Apk的过程中,会报异常Error:Execution failed for task ':app:transformClassesWithAutoPatchTransformForRelease'.> auto patch end successfully,不用紧张,其实已经生成了补丁包patch.jar,在app/build/outputs/robust/patch.jar。
〔两行哥〕带你踩坑Robust热修复_第11张图片
图11 编译生成补丁包

坑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

你可能感兴趣的:(〔两行哥〕带你踩坑Robust热修复)