background
There are tinker from wechat, Robust from Meituan, Super patch from Qzone, Andfix and Sofix from Ali. There are basically two kinds of routines
- Bottom replacement scheme, more restrictions, good time
- The class loading scheme has poor timeliness and needs to be restarted to take effect, but it has few restrictions and a wide repair range
However, these open source repair frameworks are for App repair, and there is no SOLUTION based on SDK. The company’s product provides SDK for third-party integration, which is an AAR package containing code files and resource files. Therefore, a hot repair solution for AAR is also needed.
The principle of Robust
Here’s the official schematic:Load patch.dex, replace the changeRedirect class in the class to be fixed, call changeQuickRedirect’s isSupport method every time the method is called. If the method returns false, the old method is executed. If true is returned, patchedMethod in the Patch class will be used.
The name of the package to be staked is declared in the configuration file. During compilation, the Robust adds a ChangeRedirect class to all the classes in the declared package name, and a line of code precedes each method
PatchProxyResult var3 = PatchProxy.proxy(new Object[]{postcard, callback}, this, changeQuickRedirect, false, 5447, new Class[]{Postcard.class, InterceptorCallback.class}, Void.TYPE); if (! var3.isSupported) { }Copy the code
Now what does this PatchProxy do
public static PatchProxyResult proxy(Object[] paramsArray, Object current, ChangeQuickRedirect changeQuickRedirect, boolean isStatic, int methodNumber, Class[] paramsClassTypes, Class returnType) { PatchProxyResult patchProxyResult = new PatchProxyResult(); // The main logic lies in this, to determine whether the method needs to execute patchMethod, IsSupport (paramsArray, Current, changeQuickRedirect, isStatic, methodNumber, paramsClassTypes, returnType)) { patchProxyResult.isSupported = true; patchProxyResult.result = PatchProxy.accessDispatch(paramsArray, current, changeQuickRedirect, isStatic, methodNumber, paramsClassTypes, returnType); } return patchProxyResult; }Copy the code
The PatchProxy. Finally returned to the proxy method patchProxyResult, Patch. The proxy inside of the two main methods, PatchProxy. IsSupport and PatchProxy accessDispatch method, What is the logic of isSupport
public static boolean isSupport(Object[] paramsArray, Object current, ChangeQuickRedirect changeQuickRedirect, Boolean isStatic, int methodNumber, Class[] paramsClassTypes, Class returnType) {//Robust takes priority, If (changeQuickRedirect == null) {// No patch is executed, Other listener if polling (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; } // Get the method name, ClassName = className + ":" + isStatic + ":" + isStatic + ":" + methodNumber String classMethod = getClassMethod(isStatic, methodNumber); if (TextUtils.isEmpty(classMethod)) { return false; Object[] objects = getObjects(paramsArray, current, isStatic); try { return changeQuickRedirect.isSupport(classMethod, objects); } catch (Throwable t) { return false; }}Copy the code
This method is mainly to process the method name and the parameters required by the method, and finally pass to the changeRedirect class to judge, and then take a look at the changeRedirect implementation of the class inside the judgment logic
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()); }Copy the code
The Robust generates a methodId for each method, and the getRollBack is the rollBackListener passed in by the caller to determine the rollback state of the method.
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; }Copy the code
SdkManagerpatch is the name of the class that needs to be fixed in the demo. The patch.jar generated in the Robust is prefixed with the name of the repaired class.
SDK solution based on Robust
This project was proposed by @feelschaotic, thanks to FeelsChaotic, big link: juejin.cn/post/684490…
Only in application mode can the Robust be staked and patched, apK file content and AAR package file content. Therefore, the SDK module that needs to be hot updated can be set to Application mode when packaging, and then hook compilation process. Package the files generated in Application mode into an AAR file output.
Then hook processReleaseResources Task modifiers the resource ID
In the original resource integration plugin, the file path is only adapted to the general situation of debug and release. If multiple channels are set in the project, the compiled output file path will be a directory with multiple channel names in front of the corresponding release folder.Therefore, adaptation for multi-channel version packaging needs to be added to packPlugin. The packPlugin was modified as follows
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
}
}
}
}
Copy the code