RocooFix源码分析

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 :prepareClosurecopyMappingClosure 方法的执行 应该是在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,从代码可以看出,是用代码调用了Androidbuild-toolsdex工具,将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);
        }


    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,082评论 5 464
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,231评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,047评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,977评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,893评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,976评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,605评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,888评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,906评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,732评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,513评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,980评论 3 301
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,132评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,447评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,027评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,232评论 2 339

推荐阅读更多精彩内容