最近公司准备集成第三方的热修复,于是我去对美团热修复做了个调研。在此,准备将集成的过程记录下。
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 | 1.集成了Robust后,生成apk。保存期间的混淆文件 mapping.txt,以及 Robust 生成记录文件 methodMap.robust |
集成步骤
添加依赖
在整个项目的build.gradle加入classpath1
2
3
4
5
6
7
8
9buildscript {
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)手动Copy一份robust.xml的配置文件到项目的src同级目录下,该文件各个配置注释的很清楚,若没特殊要求,不需要修改
主要配置了Robust相关的内容,如是否打开Robust,是否强制插入代码,需要热补的包名或类名,是否开启混淆,补丁的包名等。
这里主要注意下需要热补的包名hotfixPackage和补丁的包名patchPackname。patchPackname需要在修复的配置类PatchManipulateImp中设置下PatchesInfoImplClassFullName,也就是最终生成的补丁类所在包名,补丁类是PatchesInfo的实现类
打正式包,并生成mapping.txt文件和methodsMap.robust文件:
先将项目进行混淆,执行如下命令打成正式包:
1
gradlew clean assembleRelease --stacktrace --no-daemon
得到mapping.txt文件和methodsMap.robust文件之后,将得到的以上两个文件放到app/robust文件夹下,也 就是和src同级的目录。
制作patch.jar补丁包文件
打开自动补丁插件,并sync now!
1
2apply 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 下工程再重新尝试打包命令。
- 验证补丁包
将outputs/robust/patch.jar包push到手机对应的/sdcard/robust/patch_temp.jar上。
生成补丁常见问题
1 | 1.补丁生效问题。 |
注意事项
- 内部类的构造方法是private(private会生成一个匿名的构造函数)时,需要在制作补丁过程中手动修改构造方法的访问域为public
- 对于方法的返回值是this的情况现在支持不好,比如builder模式,但在制作补丁代码时,可以通过如下方式来解决,增加一个类来包装一下(如下面的B类),
1 | method a(){ |
改为
1 | method a(){ |
- 字段增加能力内测中,不过暂时可以通过增加新类,把字段放到新类中的方式来实现字段增加能力
- 新增的类支持包括静态内部类和非内部类
- 对于只有字段访问的函数无法直接修复,可通过调用处间接修复
- 构造方法的修复内测中
- 资源和so的修复内测中
- 制作补丁的时候需要和生成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 | /** |
把下载的补丁文件保留在localPath中,每次加载补丁之前先对localpath的补丁进行md5校验,然后解密补丁,并放到tempPath,补丁加载之后就删除tempPath下的文件。
关于补丁的校验这块,建议采用非对称加密,各个App根据自己业务方定制,自由发挥吧。
- 补丁加载策略
上面说了补丁加密和解密的策略,这部分重点介绍补丁如何加载才能保证尽可能修复所有问题,并且在后台做到补丁可控,可控的意思就是可以随时不使用补丁,补丁的加载与否完全由后台来决定。
首先来说为了提高补丁的加载率,有必要在应用启动的时候加载缓存的补丁,也就是上一次App启动加载过得补丁,但是这会导致一个问题,那就是如何在后台控制不使用这个补丁,这个问题可以在本地保留一个和服务器同步的补丁列表,当发现补丁被在补丁后台不存在的时候,就删除本地缓存的补丁。
- 补丁相关数据的上报
在执行new PatchExecutor(getApplicationContext(), new PatchManipulateImp(), new Callback()).start()的需要传递实现RobustCallBack接口的类,在这里进行数据的统计和上报。
1 | class Callback implements RobustCallBack { |