RocooFix源碼分析

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 :prepareClosurecopyMappingClosure 方法的執(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)用了Androidbuild-toolsdex工具盈厘,將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);
        }


    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末菲语,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子惑灵,更是在濱河造成了極大的恐慌山上,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,839評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件英支,死亡現(xiàn)場(chǎng)離奇詭異佩憾,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)干花,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門妄帘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人池凄,你說我怎么就攤上這事抡驼。” “怎么了肿仑?”我有些...
    開封第一講書人閱讀 153,116評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵婶恼,是天一觀的道長(zhǎng)桑阶。 經(jīng)常有香客問我,道長(zhǎng)勾邦,這世上最難降的妖魔是什么蚣录? 我笑而不...
    開封第一講書人閱讀 55,371評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮眷篇,結(jié)果婚禮上萎河,老公的妹妹穿的比我還像新娘。我一直安慰自己蕉饼,他們只是感情好虐杯,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,384評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著昧港,像睡著了一般擎椰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上创肥,一...
    開封第一講書人閱讀 49,111評(píng)論 1 285
  • 那天达舒,我揣著相機(jī)與錄音,去河邊找鬼叹侄。 笑死巩搏,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的趾代。 我是一名探鬼主播贯底,決...
    沈念sama閱讀 38,416評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼撒强!你這毒婦竟也來(lái)了禽捆?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,053評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤飘哨,失蹤者是張志新(化名)和其女友劉穎胚想,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體杖玲,經(jīng)...
    沈念sama閱讀 43,558評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,007評(píng)論 2 325
  • 正文 我和宋清朗相戀三年淘正,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了摆马。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,117評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鸿吆,死狀恐怖囤采,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情惩淳,我是刑警寧澤蕉毯,帶...
    沈念sama閱讀 33,756評(píng)論 4 324
  • 正文 年R本政府宣布乓搬,位于F島的核電站,受9級(jí)特大地震影響代虾,放射性物質(zhì)發(fā)生泄漏进肯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,324評(píng)論 3 307
  • 文/蒙蒙 一棉磨、第九天 我趴在偏房一處隱蔽的房頂上張望江掩。 院中可真熱鬧,春花似錦乘瓤、人聲如沸环形。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)抬吟。三九已至,卻和暖如春统抬,著一層夾襖步出監(jiān)牢的瞬間火本,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工蓄喇, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留发侵,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,578評(píng)論 2 355
  • 正文 我出身青樓妆偏,卻偏偏與公主長(zhǎng)得像刃鳄,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子钱骂,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,877評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容