Shiply 热修复常见使用问题
1. 集成RFix后编译失败,提示:'XXX cannot be cast to java.util.function.Supplier'?
解决方案:在项目根目录下的build.gradle
中指定guava的版本
buildscript {
dependencies {
// 添加该依赖是为了解决一个Android插件的强转异常:java.util.function.Supplier
// https://github.com/GoogleCloudPlatform/artifact-registry-maven-tools/issues/27
classpath 'com.google.guava:guava:27.1-jre'
}
}
2. 编译Tinker补丁失败,提示:'XXX which is not in loader class'?
2.1. 原因分析
下图是Tinker的一个结构图,在Application改造的时候,我们将应用的Dex拆分成了两部分,通过ApplicationLike的方式来实现Application的代理
这种结构要求Loader中的类是不能直接访问Bussiness中的类的,因此补丁制作工具会分析Class的依赖关系,如果发现了这类依赖存在就会出现编译失败
2.2. 引起上述依赖的行为
2.2.1. 业务的自动插桩工具(数据统计、行为监控、组件替换等)
下面的异常就是matrix性能监控对函数插桩,导致Loader的Class对Matrix的Class产生了依赖,从而抛出了异常
解决方案:这类问题的解决需要业务对引发插桩的组件进行白名单配置,忽略以下包名的类:
com.tencent.rfix.loader.*
com.tencent.tinker.loader.*
2.2.2. Application中引入业务逻辑
理论上在进行了ApplicationLike的代理后,Application中是不应该存在业务逻辑的,但是某些场景(如:重载Application的部分接口)
这种状态不得不在Application中引入业务逻辑,从而产生Loader的Class对Bussiness的Class的依赖,导致补丁构建失败
解决方案:这类问题建议将引入的依赖逻辑封装到单独的类中,然后通过反射的方式进行调用,解除Class的直接依赖
3. 如何对多架构版本(32+64+混合)同时下发补丁?
3.1. 方案一:统一构建,统一下发
RFix的配置支持多组old.apk
和new.apk
参数,在补丁制作时会同时进行diff,生成一个补丁包
补丁下发系统还是下发一个包,客户端下载到补丁后,再根据自己的情况选择使用哪个包
RFix配置参数:
RFixPatch {
// 单架构打包
oldApk = "${projectDir.absolutePath}/RFix/old.apk"
newApk = "${projectDir.absolutePath}/RFix/new.apk"
// 多架构打包
oldApks = ["${projectDir.absolutePath}/RFix/old.apk",
"${projectDir.absolutePath}/RFix/old64.apk"]
newApks = ["${projectDir.absolutePath}/RFix/new.apk",
"${projectDir.absolutePath}/RFix/new64.apk"]
}
RFix补丁包结构:
3.2. 方案二:统一构建,按需下发
该方案是在方案一
的基础上,将构建时生成的补丁拆分成多个独立的补丁包
补丁下发系统会下发多个URL,客户端根据自己的情况选择性下载合适的补丁包
PS:该方案只是为了优化补丁下发的CDN成本,如果没有这方面的需求可以忽略
RFix配置参数:
RFixPatch {
buildConfig {
// 开启补丁包独立打包功能
// 支持版本:1.1.0+
enablePackageSeparate = true
}
}
RFix补丁产物:
3.3. 如何制作多架构版本补丁
第一步:准备好old.apk、old64.apk,以及它们各自对应的mapping.txt、R.txt
PS:这里的混淆规则文件不要弄混
第二步:在补丁分支分别构建new.apk、new64.apk
PS:这里需要特别注意的是,Apk编译时的混淆规则参数applyMapping
和applyResourceMapping
并不区分32/64
因此在构建new.apk、new64.apk时需要注意,不要配置错了使用的混淆规则文件
第三步:确认两组产物都放到了配置的位置后,执行RFixBuildRelease
4. RFix补丁包过大的问题?
4.1. 原因分析
补丁的制作过程本质上是对新旧两个Apk进行Diff的过程,为了保证Diff过程能真正的提取出代码变更的点
我们在进行new.apk
的编译时会应用old.apk
的混淆文件(mappint.txt、R.txt),保证没有修改的代码和资源不会发生变化
但是因为一些特殊的原因,只应用旧Apk的混淆文件并不能保证新Apk的一致性,从而导致了补丁包的差异过大
4.2. 影响补丁包大小的原因
4.2.1. 构建new.apk时没有应用old.apk的混淆规则
这个问题在刚接入的时候会比较容易出现,因为还没有理解应用混淆规则的目的
4.2.2. 补丁分支上存在修改混淆规则的提交
补丁分支上修改混淆规则,被影响的这部分类和函数就没法版本再应用old.apk的mapping.txt
这必然导致大量的类或函数差异,严重的还会影响原有dex的分包策略,造成更大的差异
4.2.3. 使用了自动插桩的性能监控组件(如:Matrix)
Matrix的性能监控会在每个函数的开始插入一个函数调用,并为每个函数分配一个编号
这就导致两次编译可能同一个函数的编号是不一致的,从而会引入大量的差异代码
解决方案:编译old.apk之后需要备份methodMap,在构建new.apk时使用旧的methodMap
4.2.4. 使用了第三方的Apk压缩组件(如:AndResGuard)
AndResGuard会对资源文件进行混淆,通过压缩资源文件的名字来减少安装包的大小
因此每次编译,混淆的规则可能是不一样的,这也会导致前后两次的补丁存在较大的差异
解决方案:编译old.apk之后AndResGuard会生成一个资源混淆的mapping文件,在构建new.apk时需要配置使用该文件来产生相同的混淆结果
5. 如何提取构建产物中的mapping.txt
和R.txt
?
不同的AGP版本在Apk编译后,存放相关的产物目录会有些许的差异,业务在AGP升级的时候,补丁最容易出现的问题就是混淆规则文件提取失败
[gradle/android-auto-backup.gradle]可以自动的备份补丁依赖的构建产物,可以参考或者使用这个脚本来辅助产物提取
6. 加固后的App加载补丁失败的问题(乐固)?
6.1. 原因分析
经过加固的Apk是无法正常的提取到安装包中的Dex文件,这就导致加固Apk是不能用来进行DexDiff的
如果直接使用加固后的Apk来进行补丁的生成,运行时是无法正常加载的
6.2. 解决方案
PS:只有开启深度加固(隐藏原Apk的dex、so、res)时才需要按如下方案解决,否则不需要做特殊处理
第一步:补丁构建流水线进行补丁制作时使用的old.apk和new.apk都必须是非加固的包(因为加固后的Apk是无法提取到原Apk数据的)
第二步:配置RFix的补丁编译参数,标记当前制作的补丁为加固App使用
RFixPatch {
buildConfig {
// 应用是否加固
isProtectedApp = true
}
}