RocooFix很重要的一部分就是他的gradle插件厕九,本文著重記錄插件部分,而且主要針對(duì)gradle1.4以上的情況
插件(buildsrc)
RocooFix解決了nuwa不能在gradle1.4以上生效,主要是1.4以上引入的 transform
API(官網(wǎng)解釋The API existed in 1.4.0-beta2 but it's been completely revamped in 1.5.0-beta1)霎桅,導(dǎo)致preDexTask
dexTask
proguardTask
都不能被直接find到(因?yàn)楦甙姹镜?code>gradle換了task的名字 transformxxxx,而nuwa的name是寫死的旅择,導(dǎo)致不能被findByName
找到宣赔,RocooFix通過判斷當(dāng)先的gradle版本來(lái)確定是不是加上transformxxxx)预麸。 在1.4版本以上,修復(fù)的主要邏輯在 if(preDexTask)
這個(gè)判斷語(yǔ)句的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
}
先創(chuàng)建了名字為 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的用法)钩蚊,因?yàn)檩斎胛募ㄎ募臀募A贡翘,分情況將文件和文件夾放入文件set中
- 如果是文件夾,把文件夾內(nèi)后綴為jar的文件取出放入set
- 如果文件夾的絕對(duì)路徑以
intermediates/classes + variant.dirName
結(jié)尾(文件夾里都是.class文件)砰逻,就把這個(gè)文件夾放到set - 如果是文件鸣驱,而且后綴是jar就把這個(gè)文件放入set
獲取到所有的輸入文件后,對(duì)其進(jìn)行統(tǒng)一處理蝠咆,其實(shí)也是針對(duì)jar文件和class文件踊东。
對(duì)于jar文件,則直接沿用nuwa的處理方式NuwaProcessor#processJar
刚操,就不貼出這個(gè)方法的代碼了闸翅,大概要實(shí)現(xiàn)的就是判斷輸入的文件(jar)中的類是否要注入Hack
(處理ISPREVERIFIED),需要注入的類以操作字節(jié)碼的形式注入Hack類到構(gòu)造函數(shù)里菊霜,聽過美團(tuán)的robust
的知乎live缎脾,據(jù)說這樣處理不會(huì)增加方法數(shù)是因?yàn)楸緛?lái)都會(huì)給每一個(gè)類增加一個(gè)默認(rèn)的構(gòu)造函數(shù),所以操作構(gòu)造函數(shù)不會(huì)增加方法數(shù)(字節(jié)碼操作使用開源庫(kù)asm)占卧。值得一提的是,NuwaProcessor#processJar
這個(gè)方法還將mapping
文件傳入联喘,是為了處理混淆的情況华蜒,mapping文件保存的是上一次的混淆配置,使用這個(gè)才能讓補(bǔ)丁類定位到真正的打補(bǔ)丁的位置豁遭,要不然會(huì)gg叭喜。
接下來(lái)就到了下一個(gè)else if語(yǔ)句中,這段分支語(yǔ)句就是處理之前說的文件set的第二點(diǎn)蓖谢,文件夾intermediates/classes/xxxx
捂蕴,里面放置的都是class文件,針對(duì)class進(jìn)行處理闪幽。它還是用FileUtils.listFiles
方法取出這些文件夾中的.class文件以一個(gè)文件set保存啥辨,接著遍歷這個(gè)set,剔除不應(yīng)該注入的類(R文件類盯腌,BuildConfig相關(guān)類溉知,在gradle中標(biāo)注不需要熱修復(fù)的類等等),后調(diào)用NuwaProcessor#processClass
這個(gè)方法來(lái)處理應(yīng)該注入Hack
到構(gòu)造函數(shù)中的類,還是字節(jié)碼啦级乍。 之后就是生產(chǎn)hash文件的邏輯了舌劳。
跳出處理文件和插入字節(jié)碼的task就是處理每一個(gè)task順序的問題,可以看到rocooJarBeforeDexTask
要依賴于dexTask.taskDependencies.getDependencies(dexTask)
玫荣,也就是原來(lái)的dexTask
之前的任務(wù)(將class/jar打包為dex之前的任務(wù)) 甚淡。也就是rocooJarBeforeDexTask
要在原本dexTask
之前的任務(wù)的之后,在執(zhí)行rocooJarBeforeDexTask
開始的時(shí)候doFirst
執(zhí)行prepareClosure
閉包的任務(wù)捅厂,在執(zhí)行rocooJarBeforeDexTask
結(jié)束的時(shí)候通過doLast
執(zhí)行copyMappingClosure
閉包的任務(wù)贯卦。rocooPatchTask
(制作補(bǔ)丁的task)在rocooJarBeforeDexTask
之后執(zhí)行,之后原本的dexTask
要在制作補(bǔ)丁之后執(zhí)行恒傻。
所以順序是這樣的 :原本dexTask之前就要執(zhí)行的task -> 字節(jié)碼注入的task -> prepareClosure -> copyMappingClosure -> 制作補(bǔ)丁的(taskrocooPatchTask) -> dexTask(字節(jié)碼到dex)
ps :prepareClosure
和 copyMappingClosure
方法的執(zhí)行 應(yīng)該是在rocooJarBeforeDexTask
任務(wù)開始和結(jié)束的時(shí)候執(zhí)行
def rocooJarBeforeDexTask = project.tasks[rocooJarBeforeDex]
rocooJarBeforeDexTask.dependsOn dexTask.taskDependencies.getDependencies(
dexTask)
rocooJarBeforeDexTask.doFirst(prepareClosure)
rocooJarBeforeDexTask.doLast(copyMappingClosure)
rocooPatchTask.dependsOn rocooJarBeforeDexTask
dexTask.dependsOn rocooPatchTask
RocooFix
還封裝了從補(bǔ)丁類到dex的功能RocooUtils#makeDex
脸侥,從代碼可以看出,是用代碼調(diào)用了Android
的build-tools
的dex
工具盈厘,將jar打包為Android運(yùn)行的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()
// 注意看這里 調(diào)用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 {
}
}
}
修復(fù)Libs(RocooFix)
dex插入就不說了,已經(jīng)有很多現(xiàn)有的優(yōu)秀文章了沸手,值得一提的是RocooFix
支持runningTimeFix
外遇,和普通Java修復(fù)的方式不同的是,他使用了Legend 也就是nativeHook
的形式契吉,實(shí)現(xiàn)了即時(shí)修復(fù)的效果跳仿,同阿里系的nativeHook
修復(fù)方式,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);
}
}