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);
        }


    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章