背景
市面上的热修复方案大厂开源的有微信的tinker、美团的Robust、Qzone 的超级补丁、阿里系的Andfix、Sofix; 实现的套路套路基本就两种
- 底层替换方案,限制多,时效好
- 类加载方案,时效性差,需要重启才生效,但是限制少,修复范围比较广 但是这些开源的修复框架都是针对App做修复的,没有基于SDK的解决方案,公司的产品是提供SDK给第三方集成,是含有代码文件以及资源文件的aar包,因此也需要一套可以针对aar 的热修复解决方案。
Robust 的原理
以下是官方原理图: 加载patch.dex ,替换掉要修复的类中的changeRedirect 类,每次调用调用方法的时候都会调用changeQuickRedirect 的isSupport 方法,如果该方法返回的是false,则执行原先的旧的方法,如果返回为true,则会走patch类中的patchedMethod。
配置文件中声明了需要插桩的包名,Robust 在编译的时候会对声明的这个包名中的所有的类中增加一个ChangeRedirect类,每个方法的前面增加一段代码
PatchProxyResult var3 = PatchProxy.proxy(new Object[]{postcard, callback}, this, changeQuickRedirect, false, 5447, new Class[]{Postcard.class, InterceptorCallback.class}, Void.TYPE);
if (!var3.isSupported) {
}
接着看下这个PatchProxy做了什么
public static PatchProxyResult proxy(Object[] paramsArray, Object current, ChangeQuickRedirect changeQuickRedirect, boolean isStatic, int methodNumber, Class[] paramsClassTypes, Class returnType) {
PatchProxyResult patchProxyResult = new PatchProxyResult();
// 主要的逻辑在于这,判断这个方法的是否需要执行patchMethod,如果需要则调accessDispatch方法执行patchMethod
if (PatchProxy.isSupport(paramsArray, current, changeQuickRedirect, isStatic, methodNumber, paramsClassTypes, returnType)) {
patchProxyResult.isSupported = true;
patchProxyResult.result = PatchProxy.accessDispatch(paramsArray, current, changeQuickRedirect, isStatic, methodNumber, paramsClassTypes, returnType);
}
return patchProxyResult;
}
这个PatchProxy.proxy 方法最后返回了个patchProxyResult,Patch.proxy 里面主要调了两个方法,PatchProxy.isSupport和PatchProxy.accessDispatch 方法,再往里看下isSupport 的判断逻辑是什么
public static boolean isSupport(Object[] paramsArray, Object current, ChangeQuickRedirect changeQuickRedirect, boolean isStatic, int methodNumber, Class[] paramsClassTypes, Class returnType) {
//Robust补丁优先执行,其他功能靠后
if (changeQuickRedirect == null) {
//不执行补丁,轮询其他监听者
if (registerExtensionList == null || registerExtensionList.isEmpty()) {
return false;
}
for (RobustExtension robustExtension : registerExtensionList) {
if (robustExtension.isSupport(new RobustArguments(paramsArray, current, isStatic, methodNumber, paramsClassTypes, returnType))) {
robustExtensionThreadLocal.set(robustExtension);
return true;
}
}
return false;
}
// 获取方法名,拼接成这样的形式 classMethod = className + ":" + methodName + ":" + isStatic + ":" + methodNumber
String classMethod = getClassMethod(isStatic, methodNumber);
if (TextUtils.isEmpty(classMethod)) {
return false;
}
// 获取执行方法所需要的参数,current 为当前类的实例
Object[] objects = getObjects(paramsArray, current, isStatic);
try {
return changeQuickRedirect.isSupport(classMethod, objects);
} catch (Throwable t) {
return false;
}
}
这个方法里面主要是处理方法名和方法所需要的参数,最后再传给changeRedirect类去判断,再看下changeRedirect的实现类里面的判断逻辑
public boolean isSupport(String methodName, Object[] paramArrayOfObject) {
String str = methodName.split(":")[3];
this.methodsId = ":7:";
this.methodLongName = "com.feelschaotic.samplesdk.manager.SdkManager.callBug();";
if (RollbackManager.getInstance().getRollback(":7:")) {
return false;
}
return ":7:".contains(new StringBuffer().append(":").append(str).append(":").toString());
}
Robust 会给每个方法生成一个methodId,getRollBack 是调用方传进去的rollBackListener,判断该方法的回滚状态。
public Object accessDispatch(String methodName, Object[] paramArrayOfObject) {
SdkManagerPatch sdkManagerPatch;
try {
if (!methodName.split(":")[2].equals("false")) {
sdkManagerPatch = new SdkManagerPatch(null);
} else if (keyToValueRelation.get(paramArrayOfObject[paramArrayOfObject.length - 1]) == null) {
sdkManagerPatch = new SdkManagerPatch(paramArrayOfObject[paramArrayOfObject.length - 1]);
keyToValueRelation.put(paramArrayOfObject[paramArrayOfObject.length - 1], null);
} else {
sdkManagerPatch = (SdkManagerPatch) keyToValueRelation.get(paramArrayOfObject[paramArrayOfObject.length - 1]);
}
if ("7".equals(methodName.split(":")[3])) {
sdkManagerPatch.callBug();
}
} catch (Throwable th) {
RollbackManager.getInstance().notifyOnException(this.methodsId, this.methodLongName, th);
th.printStackTrace();
}
return null;
}
里面主要做了一个static method和 非static method 的区分,后面的修复方法的执行就交给了sdkManagerpatch,SdkManager 是demo 中需要修复的类的名称,Robust 中生成的patch.jar 都以修复的类名做前缀。
基于Robust的SDK解决方案
这个方案是 @FeelsChaotic 大佬提出的,感谢大佬,大佬原文链接:juejin.cn/post/684490…
Robust 只有在application 模式下才能进行插桩以及生成补丁,apk的文件内容和aar 包的文件内容,因此可以在需要进行热更新的sdk模块在打包的时候设置成application 模式,之后在hook编译的过程,把application 模式下生成的文件打包成aar文件输出。
之后再hook processReleaseResources Task修改资源ID的修饰符
OK,改完一跑没东西输出,原来的资源整合的plugin 中对文件的路径只适配了一般的debug和release的情况,项目中如果设置了多个渠道的话编译输出的文件路径会在对应的release 文件夹前面多一级渠道名的目录。 因此需要在packplugin 中增加对多渠道版本打包的适配。 最后修改之后的packplugin 如下
import java.util.regex.Matcher
import java.util.regex.Pattern
final String SDK_PACKAGE_DIR = sdkPackageName.replace('.', File.separator)
final String PACK_PREFIX = 'sdk_hotfix'
String JAR_TASK_NAME = 'jar_' + PACK_PREFIX
String AAR_TASK_NAME = 'aar_' + PACK_PREFIX
String PATH = projectDir.toString() + File.separator + 'robustjar' + File.separator + 'release'
hookBuild(SDK_PACKAGE_DIR)
hookAssembleAndCopyRes(PATH, JAR_TASK_NAME, AAR_TASK_NAME)
hookBundle(PATH)
private void hookBuild(sdkPackageDir) {
tasks.whenTaskAdded { task ->
if (!isAppModule.toBoolean()) {
// 不是Application模式不处理
return
}
Pattern p = Pattern.compile("^process(.*)ReleaseResources\$")
Matcher m = p.matcher(task.name)
if (!m.find()) {
return
}
String flavorName = task.name.minus("process").minus("ReleaseResources")
println '-- hookBuild 监听task:' + task.name + "flavorName: " + flavorName
task.doLast {
// hook所需资源所在的父目录, \sdk\build\generated
String generatedPath = buildDir.toString() + File.separator + "generated"
// R资源文件的路径,\sdk\build\generated\r\release\包名\R.java
String rPath = generatedPath + File.separator + "source" + File.separator + "r" + File.separator + flavorName + File.separator + "release" + File.separator + sdkPackageDir + File.separator + "R.java"
println '--R文件路径:' + rPath
File file = new File(rPath)
if (file.exists()) {
println '--R文件存在,开始修改修饰符'
ant.replace(
file: rPath,
token: 'public static final int',
value: 'public static int'
) {
fileset(file: rPath)
}
println '--R文件修改完成!'
} else {
println '--【告警】R文件不存在!'
}
}
}
}
private void hookAssembleAndCopyRes(path, jarTaskName, aarTaskName) {
// 项目打release版本apk包的话,必然会调用到assemble(渠道)Release的命令,于是我们可以用正则匹配来匹配所有渠道的打Release包过程
Pattern p = Pattern.compile("^assemble(.*)Release\$")
// 在task添加到列表的时候,进行打包task的匹配
tasks.whenTaskAdded { task ->
if (!isAppModule.toBoolean()) {
// 不是Application模式不处理
return
}
// 在任务执行的时候,匹配执行assemble(渠道)Release的打APK任务
Matcher m = p.matcher(task.name)
if (!m.find()) {
return
}
// 打release包task完成之后进行资源的整合以及jar包去指定class文件,并且生成aar包
task.doLast {
String flavorName = task.name.minus("assemble").minus("Release").toLowerCase()
if (flavorName.isEmpty())
return
path = projectDir.toString() + File.separator + 'robustjar'+ File.separator + flavorName + File.separator + 'release'
println '-- hookAssembleAndCopyRes 监听task:' + task.name + " flavorName: " + flavorName
delete {
// 删除上次生成的文件目录,目录为 ${path}
delete projectDir.toString() + File.separator + 'robustjar' + File.separator + flavorName + File.separator + "release"
}
// 打包所需资源所在的父目录, \sdk\build\intermediates
String intermediatesPath = buildDir.toString() + File.separator + "intermediates"
// gradle-3.0.0 & robust-0.4.71对应的路径为 \sdk\build\intermediates\transforms\proguard\release\0.jar
String jarDirName = (isProguard.toBoolean() ? "proguard" : "robust") + File.separator + flavorName
String robustJarPath = intermediatesPath + File.separator + "transforms" + File.separator + jarDirName + File.separator + "release" + File.separator + "0.jar"
// gradle-2.3.3 & robust-0.4.7对应的路径为 \sdk\build\intermediates\transforms\proguard\release\jars\3\1f\main.jar
// String robustJarPath = intermediatesPath + File.separator + "transforms" + File.separator + "proguard" + File.separator + "release" + File.separator + "jars" + File.separator + "3" + File.separator + "1f" + File.separator + "main.jar"
// 资源文件的路径,\sdk\build\intermediates\assets\release
String assetsPath = intermediatesPath + File.separator + "assets" + File.separator + flavorName + File.separator + "release"
// 依赖本地jar包路径,\sdk\build\intermediates\jniLibs\release
String libsPath = intermediatesPath + File.separator + "jniLibs" + File.separator + flavorName + File.separator + "release"
// res资源文件的路径,\sdk\build\intermediates\res\merged\release,经测试发现此目录下生成的.9图片会失效,因此弃置,换另外方式处理
// String resPath = intermediatesPath + File.separator + "res" + File.separator + "merged" + File.separator + "release"
// 由于上述问题,直接用项目的res路径 \sdk\src\main\res ,因此第三方依赖的资源文件无法整合,但是我是基于生成只包含自身代码的jar包和资源,其余依赖宿主另外再依赖的方案,所以可以这样处理
String resPath = projectDir.toString() + File.separator + "src" + File.separator + "main" + File.separator + "res"
// 资源id路径,\sdk\build\intermediates\symbols\release
String resIdPath = intermediatesPath + File.separator + "symbols" + File.separator + flavorName + File.separator + "release"
// 清单文件路径,\sdk\build\intermediates\manifests\full\release,由于是生成的application的清单文件,因此下面还会做删除组件声明的处理
String manifestPath = intermediatesPath + File.separator + "manifests" + File.separator + "full" + File.separator + flavorName + File.separator + "release"
// 整合上述文件后的目标路径,${path}\origin
String destination = path + File.separator + 'origin'
// 貌似aidl的文件夹没啥用,打包会根据例如G:\\sms-hotfix\\SmsParsingForRcs-Library\\library\\src\\main\\aidl\\com\\cmic\\IMyAidlInterface.aidl的定义代码生成com.cmic.IMyAidlInterface到jar包里面,因此aidl仅仅是空文件夹
// String aidlPath = buildDir.toString() + File.separator + "generated" + File.separator + "source" + File.separator + "aidl" + File.separator + "release"
println '-- robustJarPath ' + robustJarPath
File file = file(robustJarPath)
if (!file.exists()) {
println '--【告警】robust插桩jar包不存在,结束'
return
}
println '--开始复制robust插桩jar包'
copy {
// 拷贝到assets目录
from(assetsPath) {
into 'assets'
}
// .so文件拷贝到jni目录
from(libsPath) {
into 'jni'
include '**/*/*.so'
}
// 资源文件拷贝到res目录
from(resPath) {
// 排除MainActivity加载的布局文件,因为输出的是jar包,加MainActivity仅仅是为了能让打apk包任务执行
//exclude '/layout/activity_main.xml'
exclude {
// 排除空文件夹
it.isDirectory() && it.getFile().listFiles().length == 0
}
into 'res'
}
// aidl的文件夹没啥用,不处理
// from(aidlPath) {
// into 'aidl'
// }
// 拷贝此目录下资源id文件 R.txt
from resIdPath
// 拷贝到目录 ${path}\origin
into destination
}
// 补丁生成需要的mapping.txt和methodsMap.robust文件
copy {
// 混淆mapping文件的路径,\sdk\build\outputs\mapping\release\mapping.txt
from(buildDir.toString() + File.separator + 'outputs' + File.separator + 'mapping' + File.separator + flavorName + File.separator + 'release') {
include 'mapping.txt'
}
// 拷贝到目录 ${path}
into path
}
copy {
// robust生成的methodsMap文件路径,\sdk\build\outputs\robust\methodsMap.robust
from(buildDir.toString() + File.separator + 'outputs' + File.separator + 'robust') {
include 'methodsMap.robust'
}
// 拷贝到目录 ${path}
into path
}
// 若不存在aidl目录,创建aidl空目录
createDir(destination + File.separator + "aidl")
// 同上
createDir(destination + File.separator + "assets")
// 同上
createDir(destination + File.separator + "jni")
// 同上
createDir(destination + File.separator + "libs")
// 同上
createDir(destination + File.separator + "res")
//将清单文件application节点的内容和activity节点的内容替换,将清单文件provider节点的内容和meta-data节点的内容替换
def oldStr = ["<application[\\s\\S]*?>", "<activity[\\s\\S]*?</activity>", "<provider[\\s\\S]*?(</provider>|/>)", "<meta-data[\\s\\S]*?(</meta-data>|/>)"]
def newStr = ["<application\n" + " android:allowBackup=\"false\"\n" + " android:supportsRtl=\"true\">", "", "", ""]
try {
//处理 \sdk\build\intermediates\manifests\full\release\AndroidManifest.xml
String strBuffer = fileReader(manifestPath + File.separator + "AndroidManifest.xml", oldStr, newStr)
//输出至 ${path}\origin\AndroidManifest.xml
fileWrite(destination + File.separator + "AndroidManifest.xml", strBuffer)
} catch (FileNotFoundException e) {
e.printStackTrace()
}
println '--输出robust插桩jar包成功!'
println 'task name : ' + jarTaskName + "_" + flavorName
createJarTask('jar_' + 'sdk_hotfix' + "_" + flavorName, path, sdkPackageName, flavorName)
// 执行打jar包的task,这里会做原jar包的过滤处理,只保留我们需要的代码
createAarTask('aar_' + 'sdk_hotfix' + "_" + flavorName, path)
//delete project.buildDir
}
}
}
private Task createAarTask(taskName, path) {
tasks.create(name: taskName, type: Zip) {
// aar包输出路径为 ${path}\aar
File destDir = file(path + File.separator + 'aar')
// aar包命名为 library-release.aar
archiveName 'library-release.aar'
// 源路径为 ${path}\origin
from path + File.separator + 'origin'
// 设置压缩后输出的路径
destinationDir destDir
println '--创建压缩aar包Task完毕'
}.execute()
}
private Task createJarTask(taskName, path, sdkPackageName, flavorName) {
tasks.create(name: taskName, type: Jar) {
// jar包命名为classes.jar
baseName 'classes'
String intermediatesPath = buildDir.toString() + File.separator + "intermediates"
// gradle-3.0.0 & robust-0.4.71对应的路径为 \sdk\build\intermediates\transforms\proguard\release\0.jar
String jarDirName = (isProguard.toBoolean() ? "proguard" : "robust") + File.separator + flavorName
String robustJarPath = intermediatesPath + File.separator + "transforms" + File.separator + jarDirName + File.separator + "release" + File.separator + "0.jar"
def zipFile = new File(robustJarPath)
// 将jar包解压
FileTree jarTree = zipTree(zipFile)
from jarTree
// jar包输出路径为 ${path}\origin
File destDir = file(path + File.separator + 'origin')
// 设置输出路径
setDestinationDir destDir
include {
// 只打包我们需要的类
it.path.startsWith(sdkPackageName)
}
//
// exclude {
// // println "执行排除:" + it.path
// // 排除R相关class文件,排除MainActivity.class文件
// it.path.startsWith(sdkPackageName + '/R$') || it.path.startsWith(sdkPackageName + '/R.class') || it.path.startsWith(sdkPackageName + '/MainActivity.class')
// }
println '--创建压缩jar包Task完毕--' + taskName
}.execute()
}
//读取文件并替换字符串
static def fileReader(path, oldStr, newStr) {
def readerString = new File(path).getText('UTF-8')
for (int i = 0; i < oldStr.size(); i++) {
readerString = readerString.replaceFirst(oldStr[i], newStr[i])
}
return readerString
}
//写文件
static def fileWrite(path, stringBuffer) {
new File(path).withWriter('UTF-8') {
within ->
within.append(stringBuffer)
}
}
// 创建目录
static def createDir(String destDirName) {
File dir = new File(destDirName)
if (dir.exists()) {
println '--目标目录已存在!无需创建'
return false
}
if (!destDirName.endsWith(File.separator)) {
destDirName = destDirName + File.separator
}
if (dir.mkdirs()) {
println '--创建目录成功!' + destDirName
return true
} else {
println '--创建目录失败!'
return false
}
}
//项目uploadArchives时,必然会调用到bundleRelease Task,hook bundle* Task 用于在上传maven前把本地打包的aar改为插桩后的aar
private void hookBundle(path) {
tasks.whenTaskAdded { task ->
if (isAppModule.toBoolean()) {
// 是Application模式不处理,因为Application模式没有bundleRelease Task
return
}
if (!'bundleRelease'.equals(task.name)) {
return
}
task.doFirst {
println '--hook bundleRelease!'
forEachInputs(it, path)
}
}
}
private void forEachInputs(Task it, String path) {
String jarName = 'classes.jar'
it.inputs.files.each { input ->
if (input.absolutePath.indexOf(jarName) != -1) {
String jarInputPath = input.absolutePath.substring(0, input.absolutePath.lastIndexOf(File.separator) + 1)
copy {
// 源路径为 ${path}\origin
from(path + File.separator + 'origin') {
include jarName
}
into jarInputPath
}
}
}
}