RocooFix很重要的一部分就是他的gradle插件,本文着重记录插件部分,而且主要针对gradle1.4以上的情况
插件(buildsrc)
RocooFix解决了nuwa不能在gradle1.4以上生效,主要是1.4以上引入的 transform
API(官网解释The API existed in 1.4.0-beta2 but it's been completely revamped in 1.5.0-beta1),导致preDexTask
dexTask
proguardTask
都不能被直接find到(因为高版本的gradle
换了task的名字 transformxxxx,而nuwa的name是写死的,导致不能被findByName
找到,RocooFix通过判断当先的gradle版本来确定是不是加上transformxxxx)。 在1.4版本以上,修复的主要逻辑在 if(preDexTask)
这个判断语句的else if
里面
def rocooJarBeforeDex = "rocooJarBeforeDex${variant.name.capitalize()}"
project.task(rocooJarBeforeDex) << {
Set<File> inputFiles = RocooUtils.getDexTaskInputFiles(project, variant,
dexTask)
inputFiles.each { inputFile ->
def path = inputFile.absolutePath
if (path.endsWith(SdkConstants.DOT_JAR)) {
NuwaProcessor.processJar(hashFile, inputFile, patchDir, hashMap,
includePackage, excludeClass)
} else if (inputFile.isDirectory()) {
//不处理不开混淆的情况
//intermediates/classes/debug
def extensions = [SdkConstants.EXT_CLASS] as String[]
def inputClasses = FileUtils.listFiles(inputFile, extensions,
true);
inputClasses.each { inputClassFile ->
def classPath = inputClassFile.absolutePath
if (classPath.endsWith(".class") && !classPath.contains(
"/R\$") &&
!classPath.endsWith("/R.class") &&
!classPath.endsWith("/BuildConfig.class")) {
if (NuwaSetUtils.isIncluded(classPath,
includePackage)) {
if (!NuwaSetUtils.isExcluded(classPath,
excludeClass)) {
def bytes = NuwaProcessor.processClass(
inputClassFile)
if ("\\".equals(File.separator)) {
classPath =
classPath.split("${dirName}\\\\")[1]
} else {
classPath =
classPath.split("${dirName}/")[1]
}
def hash = DigestUtils.shaHex(bytes)
hashFile.append(
RocooUtils.format(classPath, hash))
if (RocooUtils.notSame(hashMap, classPath,
hash)) {
def file = new File(
"${patchDir}${File.separator}${classPath}")
file.getParentFile().mkdirs()
if (!file.exists()) {
file.createNewFile()
}
FileUtils.writeByteArrayToFile(file, bytes)
}
}
}
}
}
}
}
}
def rocooJarBeforeDexTask = project.tasks[rocooJarBeforeDex]
rocooJarBeforeDexTask.dependsOn dexTask.taskDependencies.getDependencies(
dexTask)
rocooJarBeforeDexTask.doFirst(prepareClosure)
rocooJarBeforeDexTask.doLast(copyMappingClosure)
rocooPatchTask.dependsOn rocooJarBeforeDexTask
dexTask.dependsOn rocooPatchTask
}
先创建了名字为 rocooJarBeforeDex
的task,在task里先获取在class被打包为dex之前的所有输入文件。看下RocooUtils#getDexTaskInputFiles()
static Set<File> getDexTaskInputFiles(Project project, BaseVariant variant, Task dexTask) {
if (dexTask == null) {
dexTask = project.tasks.findByName(getDexTaskName(project, variant));
}
if (isUseTransformAPI(project)) {
def extensions = [SdkConstants.EXT_JAR] as String[]
Set<File> files = Sets.newHashSet();
dexTask.inputs.files.files.each {
if (it.exists()) {
if (it.isDirectory()) {
Collection<File> jars = FileUtils.listFiles(it, extensions, true);
files.addAll(jars)
if (it.absolutePath.toLowerCase().endsWith("intermediates${File.separator}classes${File.separator}${variant.dirName}".toLowerCase())) {
files.add(it)
}
} else if (it.name.endsWith(SdkConstants.DOT_JAR)) {
files.add(it)
}
}
}
return files
} else {
return dexTask.inputs.files.files;
}
}
他先遍历所有的输入文件(注意下FileUtils.listFiles的用法),因为输入文件包括文件和文件夹,分情况将文件和文件夹放入文件set中
- 如果是文件夹,把文件夹内后缀为jar的文件取出放入set
- 如果文件夹的绝对路径以
intermediates/classes + variant.dirName
结尾(文件夹里都是.class文件),就把这个文件夹放到set - 如果是文件,而且后缀是jar就把这个文件放入set
获取到所有的输入文件后,对其进行统一处理,其实也是针对jar文件和class文件。
对于jar文件,则直接沿用nuwa的处理方式NuwaProcessor#processJar
,就不贴出这个方法的代码了,大概要实现的就是判断输入的文件(jar)中的类是否要注入Hack
(处理ISPREVERIFIED),需要注入的类以操作字节码的形式注入Hack类到构造函数里,听过美团的robust
的知乎live,据说这样处理不会增加方法数是因为本来都会给每一个类增加一个默认的构造函数,所以操作构造函数不会增加方法数(字节码操作使用开源库asm)。值得一提的是,NuwaProcessor#processJar
这个方法还将mapping
文件传入,是为了处理混淆的情况,mapping文件保存的是上一次的混淆配置,使用这个才能让补丁类定位到真正的打补丁的位置,要不然会gg。
接下来就到了下一个else if语句中,这段分支语句就是处理之前说的文件set的第二点,文件夹intermediates/classes/xxxx
,里面放置的都是class文件,针对class进行处理。它还是用FileUtils.listFiles
方法取出这些文件夹中的.class文件以一个文件set保存,接着遍历这个set,剔除不应该注入的类(R文件类,BuildConfig相关类,在gradle中标注不需要热修复的类等等),后调用NuwaProcessor#processClass
这个方法来处理应该注入Hack
到构造函数中的类,还是字节码啦。 之后就是生产hash文件的逻辑了。
跳出处理文件和插入字节码的task就是处理每一个task顺序的问题,可以看到rocooJarBeforeDexTask
要依赖于dexTask.taskDependencies.getDependencies(dexTask)
,也就是原来的dexTask
之前的任务(将class/jar打包为dex之前的任务) 。也就是rocooJarBeforeDexTask
要在原本dexTask
之前的任务的之后,在执行rocooJarBeforeDexTask
开始的时候doFirst
执行prepareClosure
闭包的任务,在执行rocooJarBeforeDexTask
结束的时候通过doLast
执行copyMappingClosure
闭包的任务。rocooPatchTask
(制作补丁的task)在rocooJarBeforeDexTask
之后执行,之后原本的dexTask
要在制作补丁之后执行。
所以顺序是这样的 :原本dexTask之前就要执行的task -> 字节码注入的task -> prepareClosure -> copyMappingClosure -> 制作补丁的(taskrocooPatchTask) -> dexTask(字节码到dex)
ps :prepareClosure
和 copyMappingClosure
方法的执行 应该是在rocooJarBeforeDexTask
任务开始和结束的时候执行
def rocooJarBeforeDexTask = project.tasks[rocooJarBeforeDex]
rocooJarBeforeDexTask.dependsOn dexTask.taskDependencies.getDependencies(
dexTask)
rocooJarBeforeDexTask.doFirst(prepareClosure)
rocooJarBeforeDexTask.doLast(copyMappingClosure)
rocooPatchTask.dependsOn rocooJarBeforeDexTask
dexTask.dependsOn rocooPatchTask
RocooFix
还封装了从补丁类到dex的功能RocooUtils#makeDex
,从代码可以看出,是用代码调用了Android
的build-tools
的dex
工具,将jar打包为Android运行的dex文件。
public static makeDex(Project project, File classDir) {
if (classDir.listFiles() != null && classDir.listFiles().size()) {
StringBuilder builder = new StringBuilder();
def baseDirectoryPath = classDir.getAbsolutePath() + File.separator;
getFilesHash(baseDirectoryPath, classDir).each {
builder.append(it)
}
def hash = DigestUtils.shaHex(builder.toString().bytes)
def sdkDir
Properties properties = new Properties()
File localProps = project.rootProject.file("local.properties")
if (localProps.exists()) {
properties.load(localProps.newDataInputStream())
sdkDir = properties.getProperty("sdk.dir")
} else {
sdkDir = System.getenv("ANDROID_HOME")
}
if (sdkDir) {
def cmdExt = Os.isFamily(Os.FAMILY_WINDOWS) ? '.bat' : ''
def stdout = new ByteArrayOutputStream()
// 注意看这里 调用dex工具的命令行方法
project.exec {
commandLine "${sdkDir}${File.separator}build-tools${File.separator}${project.android.buildToolsVersion}${File.separator}dx${cmdExt}",
'--dex',
"--output=${new File(classDir.getParent(), PATCH_NAME).absolutePath}",
"${classDir.absolutePath}"
standardOutput = stdout
}
def error = stdout.toString().trim()
if (error) {
println "dex error:" + error
}
} else {
}
}
}
修复Libs(RocooFix)
dex插入就不说了,已经有很多现有的优秀文章了,值得一提的是RocooFix
支持runningTimeFix
,和普通Java修复的方式不同的是,他使用了Legend 也就是nativeHook
的形式,实现了即时修复的效果,同阿里系的nativeHook
修复方式,HookManager
就是Legend
中hook的类了。
private static void replaceMethod(Class<?> aClass, Method fixMethod, ClassLoader classLoader) throws NoSuchMethodException {
try {
Method originMethod = aClass.getDeclaredMethod(fixMethod.getName(), fixMethod.getParameterTypes());
HookManager.getDefault().hookMethod(originMethod, fixMethod);
} catch (Exception e) {
Log.e(TAG, "replaceMethod", e);
}
}