美团Robust调研

最近公司准备集成第三方的热修复,于是我去对美团热修复做了个调研。在此,准备将集成的过程记录下。

Robust简介

2015底Google高调发布了 Android Studio 2.0,其中最重要的新特性Instant Run,实现了对代码修改的实时生效。美团开发团队在了解 Instant Run 原理之后,实现了一个兼容性更强的热更新方案,这就是产品化的hotpatch框架—–Robust。新一代热更新系统Robust,对Android版本无差别兼容。无需发版就可以做到随时修改线上bug,快速对重大线上问题作出反应,补丁修补成功率高达99.9%。

  • 优点

    支持Android2.3-7.X版本
    高兼容性、高稳定性,修复成功率高达三个九
    补丁下发立即生效,不需要重新启动
    支持方法级别的修复,包括静态方法
    支持增加方法和类
    支持ProGuard的混淆、内联、优化等操作

  • 缺点

    暂时不支持新增字段,但可以通过新增类解决
    暂时不支持资源和 so 修复,不过这个问题不大,因为独立于 dex 补丁,已经有很成熟的方案 了,就看怎么打到补丁包中以及 diff 方案。
    对于返回值是 this 的方法支持不太好
    没有安全校验,需要开发者在加载补丁之前自己做验证
    运行效率、方法数、包体积产生了一些副作用

流行热修复库比较


大致工作流程

1
2
3
4
5
1.集成了Robust后,生成apk。保存期间的混淆文件 mapping.txt,以及 Robust 生成记录文件 methodMap.robust
2.使用注解 @Modify 或者方法 RobustModify.modify() 标注需要修复的方法
3.开启补丁插件,执行生成 apk 命令,获得补丁包 patch.jar
4.通过推送或者接口的形式,通知 app 有补丁,需要修复
5.加载补丁文件不需要重新启动应用

集成步骤

  1. 添加依赖
    在整个项目的build.gradle加入classpath

    1
    2
    3
    4
    5
    6
    7
    8
    9
    buildscript {
    repositories {
    jcenter()
    }
    dependencies {
    classpath 'com.meituan.robust:gradle-plugin:0.4.7'
    classpath 'com.meituan.robust:auto-patch-plugin:0.4.7'
    }
    }

    在App的build.gradle,加入如下依赖

    1
    2
    3
    4
    5
    //制作补丁时将这个打开,auto-patch-plugin紧跟着com.android.application
    //apply plugin: 'auto-patch-plugin'
    apply plugin: 'robust'
    compile 'com.meituan.robust:robust:0.4.7'
    0.4.7官方文档似有误,改为0.4.5
  2. 手动Copy一份robust.xml的配置文件到项目的src同级目录下,该文件各个配置注释的很清楚,若没特殊要求,不需要修改

    主要配置了Robust相关的内容,如是否打开Robust,是否强制插入代码,需要热补的包名或类名,是否开启混淆,补丁的包名等。
    这里主要注意下需要热补的包名hotfixPackage和补丁的包名patchPackname。

    patchPackname需要在修复的配置类PatchManipulateImp中设置下PatchesInfoImplClassFullName,也就是最终生成的补丁类所在包名,补丁类是PatchesInfo的实现类

  3. 打正式包,并生成mapping.txt文件和methodsMap.robust文件:

    先将项目进行混淆,执行如下命令打成正式包:

    1
    gradlew clean  assembleRelease --stacktrace --no-daemon

    ​ 得到mapping.txt文件和methodsMap.robust文件之后,将得到的以上两个文件放到app/robust文件夹下,也 就是和src同级的目录。

  1. 制作patch.jar补丁包文件

    打开自动补丁插件,并sync now!

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

    执行打包命令,会抛出异常并看到Java.lang.RuntimeException: auto patch end successfully。
    ​ 说明补丁包生成成功。在outputs/robust/文件夹下会看到两个文件,patch.dex和patch.jar。

​ 提示:如果看到的结果是“patch method is empty ,please check your Modify annotation…”,建议先clean 下工程再重新尝试打包命令。

  1. 验证补丁包

​ 将outputs/robust/patch.jar包push到手机对应的/sdcard/robust/patch_temp.jar上。

生成补丁常见问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1.补丁生效问题。
补丁每次在apply后生效,此时app如果被杀死重启补丁会失效。因为杀死进程之后,内存中所有的信息都被清除了,所以需要应用每次启动都去加载本地缓存的补丁
因此,我们应在合适的时机apply甚至可以将apply的时机提前到application的oncreate方法或者更早的时机。
2.RuntimeException("mapping line info is error "
原因: mapping文件格式读取出错,请重新下载mapping文件,不要编辑mapping文件

3.RuntimeException("getInLineMemberString cannot find inline class ,origin class is
原因:这个问题理论上不可能出现,出现就是大锅,直接去官网提isuue

4.RuntimeException(" something wrong with mappingfile ,cannot find class
原因:mapping文件和代码没有对应上,重新下载mapping文件

5.RuntimeException(" patch method is empty ,please check your Modify annotation or useRobustModify.modify() to mark modified
methods")
原因:补丁自动化插件找不到需要制作补丁的方法,可能是忘记加上注解或者没有调用RobustModify.modify(),请针对Lamda表达式和泛型方法使用
RobustModify.modify()

更多问题
补丁运行问题排查

注意事项

  1. 内部类的构造方法是private(private会生成一个匿名的构造函数)时,需要在制作补丁过程中手动修改构造方法的访问域为public
  2. 对于方法的返回值是this的情况现在支持不好,比如builder模式,但在制作补丁代码时,可以通过如下方式来解决,增加一个类来包装一下(如下面的B类),
1
2
3
method a(){
return this;
}

​ 改为

1
2
3
method a(){
return new B().setThis(this).getThis();
}
  1. 字段增加能力内测中,不过暂时可以通过增加新类,把字段放到新类中的方式来实现字段增加能力
  2. 新增的类支持包括静态内部类和非内部类
  3. 对于只有字段访问的函数无法直接修复,可通过调用处间接修复
  4. 构造方法的修复内测中
  5. 资源和so的修复内测中
  6. 制作补丁的时候需要和生成apk的代码保持一致,执行的gradle命令也应该相同(避免不同的打包命令使用不同的代码和资源生成不同flavor的apk),当被补丁的方法中包含资源id的时候尤其注意需要保证代码的一致性。

项目中的使用

  • 何时加载补丁

补丁的加载我们推荐越靠前越好,这样对bug的可修复范围就大大的增加,建议在Application启动的时候加载补丁,比如说放在OnCreate方法里面,这样做就可以保证补丁尽快加载,修复比较靠前的bug,请注意补丁的加载需要时间(补丁的加载是异步的),也就是说即使在Application里面最早加载补丁,也不能保证修复非常靠前的bug。
补丁最先加载还可能导致另一个问题,那就是对于使用multidex的项目,需要确保所有的dex都已经加载,再加载补丁,避免被补丁的类由于没有加载而导致补丁应用失败,所以需要在补丁加载之前保证所有dex都已经加载。

  • 补丁拉取校验策略

这部分需要继承并实现PatchManipulate,这个类里面有三个方法,在PatchManipulate这个类里面需要联网拉取补丁列表(List fetchPatchList(Context context))、下载补丁(boolean ensurePatchExist(Patch patch))以及校验补丁(boolean verifyPatch(Context context, Patch patch)),拉取补丁列表之后需要把补丁相关的信息初始化,并把补丁列表的数据加密保留在某处,初始化补丁相关信息的时候,请留意类Patch的一些属性,比较重要的有四个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 补丁名称,可以作为补丁的唯一标识符
*/
private String name;
/**
* 原始补丁本地保存路径,这个补丁建议是加密之后的补丁,建议放到app的私有目录
*/
private String localPath;
/**
* 补丁本地保存路径,这个是解密之后补丁的位置,这个位置建议加载之后立即删除,避免被篡改
* 建议放到app私有目录
*/
private String tempPath;
/**
* 补丁md5值,用来校验localPath补丁的完整性
*/
private String md5;

把下载的补丁文件保留在localPath中,每次加载补丁之前先对localpath的补丁进行md5校验,然后解密补丁,并放到tempPath,补丁加载之后就删除tempPath下的文件。
关于补丁的校验这块,建议采用非对称加密,各个App根据自己业务方定制,自由发挥吧。

  • 补丁加载策略

上面说了补丁加密和解密的策略,这部分重点介绍补丁如何加载才能保证尽可能修复所有问题,并且在后台做到补丁可控,可控的意思就是可以随时不使用补丁,补丁的加载与否完全由后台来决定。
首先来说为了提高补丁的加载率,有必要在应用启动的时候加载缓存的补丁,也就是上一次App启动加载过得补丁,但是这会导致一个问题,那就是如何在后台控制不使用这个补丁,这个问题可以在本地保留一个和服务器同步的补丁列表,当发现补丁被在补丁后台不存在的时候,就删除本地缓存的补丁。

  • 补丁相关数据的上报

在执行new PatchExecutor(getApplicationContext(), new PatchManipulateImp(), new Callback()).start()的需要传递实现RobustCallBack接口的类,在这里进行数据的统计和上报。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Callback implements RobustCallBack {
@Override
public void onPatchListFetched(boolean result, boolean isNet) {
System.out.println(" robust arrived in onPatchListFetched");
}

@Override
public void onPatchFetched(boolean result, boolean isNet, Patch patch) {
System.out.println(" robust arrived in onPatchFetched");
}

@Override
public void onPatchApplied(boolean result, Patch patch) {
System.out.println(" robust arrived in onPatchApplied ");

}

@Override
public void logNotify(String log, String where) {
System.out.println(" robust arrived in logNotify "+where);
}

@Override
public void exceptionNotify(Throwable throwable, String where) {
throwable.printStackTrace();
System.out.println(" robust arrived in exceptionNotify "+where);
}
}

参考资料

Android热补丁之Robust原理解析(一)
Robust源码
Android热更新方案Robust