Replugin源碼解析之replugin-plugin-gradle ---2 動態(tài)修改組件class文件

概述

該部分基礎(chǔ)知識在Gradle學(xué)習(xí)-----Gradle自定義插件Replugin源碼解析之replugin-host-gradle涉及欣鳖,不再重復(fù)累述

源碼分析

ReClassTransform入口相關(guān)類源碼如下

public class ReClassTransform extends Transform {

    private Project project
    private def globalScope

    /* 需要處理的 jar 包 */
    def includeJars = [] as Set
    def map = [:]

    public ReClassTransform(Project p) {
        this.project = p
        AppPlugin appPlugin = project.plugins.getPlugin(AppPlugin)
        // taskManager 在 2.1.3 中為 protected 訪問類型的,在之后的版本為 private 訪問類型的,
        // 使用反射訪問
        def taskManager = BasePlugin.metaClass.getProperty(appPlugin, "taskManager")
        this.globalScope = taskManager.globalScope;
    }

    @Override
    String getName() {
        return '___ReClass___'
    }

    @Override
    void transform(Context context,
                   Collection<TransformInput> inputs,
                   Collection<TransformInput> referencedInputs,
                   TransformOutputProvider outputProvider,
                   boolean isIncremental) throws IOException, TransformException, InterruptedException {

        welcome()

        /* 讀取用戶配置 */
        ReClassConfig config = project.extensions.getByName('repluginPluginConfig')

        File rootLocation = null
        try {
            rootLocation = outputProvider.rootLocation
            println ">>> rootLocation: throw----------->rootLocation=" + (rootLocation == null ? "null" :rootLocation)
        } catch (Throwable e) {
            //android gradle plugin 3.0.0+ 修改了私有變量桑嘶,將其移動到了IntermediateFolderUtils中去
            rootLocation = outputProvider.folderUtils.getRootFolder()
            println ">>> rootLocation: throw----------->"
        }
        if (rootLocation == null) {
            throw new GradleException("can't get transform root location")
        }
        println ">>> rootLocation: ${rootLocation}"
        // Compatible with path separators for window and Linux, and fit split param based on 'Pattern.quote'
        def variantDir = rootLocation.absolutePath.split(getName() + Pattern.quote(File.separator))[1]
        println ">>> variantDir: ${variantDir}"

        CommonData.appModule = config.appModule
        CommonData.ignoredActivities = config.ignoredActivities

        def injectors = includedInjectors(config, variantDir)
        if (injectors.isEmpty()) {
            copyResult(inputs, outputProvider) // 跳過 reclass
        } else {
            doTransform(inputs, outputProvider, config, injectors) // 執(zhí)行 reclass
        }
    }
...//省略
 }

查看入口transform 方法
2.1 project.extensions.getByName('repluginPluginConfig')讀取用戶在replugin插件項目的build.gradle中配置的參數(shù)拧咳,在下面的代碼中會用到干厚,比如獲取需要忽略的注入器ignoredInjectors礁蔗、需要忽略替換的ActivityignoredActivities壤短、自定義的代碼注入器customInjectors等设拟。
2.2 includedInjectors返回用戶未忽略的注入器,代碼如下

/**
     * 返回用戶未忽略的注入器的集合
     */
    def includedInjectors(def cfg, String variantDir) {//product\debug
        def injectors = []
        Injectors.values().each {
            //設(shè)置project
            it.injector.setProject(project)
            //設(shè)置variant關(guān)鍵dir
            it.injector.setVariantDir(variantDir)
            if (!(it.nickName in cfg.ignoredInjectors)) {
                injectors << it.nickName
            }
        }
        injectors
    }

Injectors為枚舉,其中定義了5種IClassInjector注入器的封裝久脯,被封裝的分別對應(yīng)操作對應(yīng)組件及resource,源碼如下

public enum Injectors {

    LOADER_ACTIVITY_CHECK_INJECTOR('LoaderActivityInjector', new LoaderActivityInjector(), '替換 Activity 為 LoaderActivity'),//替換插件中的Activity的繼承相關(guān)代碼 為 replugin-plugin-library 中的XXPluginActivity父類
    LOCAL_BROADCAST_INJECTOR('LocalBroadcastInjector', new LocalBroadcastInjector(), '替換 LocalBroadcast 調(diào)用'),//替換插件中的LocalBroadcastManager調(diào)用代碼 為 插件庫的調(diào)用代碼纳胧。
    PROVIDER_INJECTOR('ProviderInjector', new ProviderInjector(), '替換 Provider 調(diào)用'),//替換 插件中的 ContentResolver 調(diào)用代碼 為 插件庫的調(diào)用代碼
    PROVIDER_INJECTOR2('ProviderInjector2', new ProviderInjector2(), '替換 ContentProviderClient 調(diào)用'),//替換 插件中的 ContentProviderClient 調(diào)用代碼 為 插件庫的調(diào)用代碼
    GET_IDENTIFIER_INJECTOR('GetIdentifierInjector', new GetIdentifierInjector(), '替換 Resource.getIdentifier 調(diào)用') //替換 插件中的 Resource.getIdentifier 調(diào)用代碼的參數(shù) 為 動態(tài)適配的參數(shù)

    IClassInjector injector
    String nickName
    String desc

    Injectors(String nickName, IClassInjector injector, String desc) {
        this.injector = injector
        this.nickName = nickName
        this.desc = desc;
    }
}

2.3 getInputTypes() 指明當前Trasfrom要處理的數(shù)據(jù)類型,可選類型包括CONTENT_CLASS(代表要處理的數(shù)據(jù)是編譯過的Java代碼,而這些數(shù)據(jù)的容器可以是jar包也可以是文件夾)帘撰,CONTENT_JARS(包括編譯過的Java代碼和標準的Java資源)跑慕,CONTENT_RESOURCES,CONTENT_NATIVE_LIBS等骡和。在replugin-plugin-gradle中是使用Transform來做代碼插樁,所以選用CONTENT_CLASS類型相赁。
2.4doTransform方法是執(zhí)行reclass的關(guān)鍵,代碼如下

  * 執(zhí)行 Transform
     */
    def doTransform(Collection<TransformInput> inputs,
                    TransformOutputProvider outputProvider,
                    Object config,
                    def injectors) {

        /* 初始化 ClassPool */
        Object pool = initClassPool(inputs)

        /* 進行注入操作 */
        Util.newSection()
        Injectors.values().each {
            if (it.nickName in injectors) {
                println ">>> Do: ${it.nickName}"
                // 將 NickName 的第 0 個字符轉(zhuǎn)換成小寫,用作對應(yīng)配置的名稱
                def configPre = Util.lowerCaseAtIndex(it.nickName, 0)
                doInject(inputs, pool, it.injector, config.properties["${configPre}Config"])
            } else {
                println ">>> Skip: ${it.nickName}"
            }
        }

        if (config.customInjectors != null) {
            config.customInjectors.each {
                doInject(inputs, pool, it)
            }
        }

        /* 重打包 */
        repackage()

        /* 拷貝 class 和 jar 包 */
        copyResult(inputs, outputProvider)

        Util.newSection()
    }

2.4.1 initClassPool添加編譯時需要引用的到類到 ClassPool慰于。解壓對應(yīng)的jar的路徑及名字钮科,將解壓后再重新打包的jar的路徑及名字作為值,jar的名字作為key存放在map中婆赠。includeJars存放的是 android.jar的目錄绵脯、其它各種引用到的jar進行解壓后目錄佳励、及class所在目錄。

    /**
     * 初始化 ClassPool
     */
    def initClassPool(Collection<TransformInput> inputs) {
        Util.newSection()
        def pool = new ClassPool(true)
        // 添加編譯時需要引用的到類到 ClassPool, 同時記錄要修改的 jar 到 includeJars
        Util.getClassPaths(project, globalScope, inputs, includeJars, map).each {
            println "    $it"
            pool.insertClassPath(it)
        }
        pool
    }

Util.newSection()為畫一條直線長度為50個--蛆挫,代碼如下

    def static newSection() {
        50.times {
            print '--'
        }
        println()
    }

2.4.2

CtMethod:是一個class文件中的方法的抽象表示赃承。一個CtMethod對象表示一個方法。(Javassit 庫API)
CtClass:是一個class文件的抽象表示悴侵。一個CtClass(compile-time class)對象可以用來處理一個class文件瞧剖。(Javassit 庫API)
ClassPool:是一個CtClass對象的容器類。(Javassit 庫API)
.class文件:.class文件是一種存儲Java字節(jié)碼的二進制文件可免,里面包含一個Java類或者接口抓于。

遍歷Injectors枚舉中值,與用戶未忽略的注入器injectors對比浇借,如果未被忽略則執(zhí)行doInject否則打印跳過信息

2.4.3 doInject即執(zhí)行注入操作捉撮,分別對目錄下的每個class文件執(zhí)行handleDir及對每個jar文件執(zhí)行handleJar源碼如下

/**
     * 執(zhí)行注入操作
     */
    def doInject(Collection<TransformInput> inputs, ClassPool pool,
                 IClassInjector injector, Object config) {
        try {
            inputs.each { TransformInput input ->
                input.directoryInputs.each {
                    handleDir(pool, it, injector, config)
                }
                input.jarInputs.each {
                    handleJar(pool, it, injector, config)
                }
            }
        } catch (Throwable t) {
            println t.toString()
        }
    }

2.4.4
handleDir即調(diào)用各個注入器的injectClass方法,handleJar為通過jar名稱獲取map中存放的對應(yīng)解壓目錄妇垢,然后調(diào)用各個注入器的injectClass方法

 /**
     * 處理目錄中的 class 文件
     */
    def handleDir(ClassPool pool, DirectoryInput input, IClassInjector injector, Object config) {
        println ">>> Handle Dir: ${input.file.absolutePath}"
        injector.injectClass(pool, input.file.absolutePath, config)
    }

   /**
     * 處理 jar
     */
    def handleJar(ClassPool pool, JarInput input, IClassInjector injector, Object config) {
        File jar = input.file
        if (jar.absolutePath in includeJars) {
            println ">>> Handle Jar: ${jar.absolutePath}"
            String dirAfterUnzip = map.get(jar.getParent() + File.separatorChar + jar.getName()).replace('.jar', '')
            injector.injectClass(pool, dirAfterUnzip, config)
        }
    }

2.4.5 各種注入器的injectClass方法巾遭,針對class目錄,遍歷下面class文件進行處理闯估,以LoaderActivityInjector作為例子分析灼舍,源碼如下

   @Override
    def injectClass(ClassPool pool, String dir, Map config) {
        init()

        /* 遍歷程序中聲明的所有 Activity */
        //每次都new一下,否則多個variant一起構(gòu)建時只會獲取到首個manifest
        new ManifestAPI().getActivities(project, variantDir).each {
            // 處理沒有被忽略的 Activity
            if (!(it in CommonData.ignoredActivities)) {
                handleActivity(pool, it, dir)
            }
        }
    }

def private init() {
        /* 延遲初始化 loaderActivityRules */
        // todo 從配置中讀取睬愤,而不是寫死在代碼中
        if (loaderActivityRules == null) {
            def buildSrcPath = project.project(':buildsrc').projectDir.absolutePath
            def loaderConfigPath = String.join(File.separator, buildSrcPath, 'res', LOADER_PROP_FILE)

            loaderActivityRules = new Properties()
            new File(loaderConfigPath).withInputStream {
                loaderActivityRules.load(it)
            }

            println '\n>>> Activity Rules:'
            loaderActivityRules.each {
                println it
            }
            println()
        }
    }

2.4.6 handleActivity代碼如下,主要作用就是更改Activity的最頂層父類片仿,對應(yīng)關(guān)系為loaderActivityRules數(shù)組。

 private def handleActivity(ClassPool pool, String activity, String classesDir) {
        def clsFilePath = classesDir + File.separatorChar + activity.replaceAll('\\.', '/') + '.class'
        if (!new File(clsFilePath).exists()) {
            return
        }

        println ">>> Handle $activity  &clsFilePath=-----------> $clsFilePath"

        def stream, ctCls
        try {
            stream = new FileInputStream(clsFilePath)
            ctCls = pool.makeClass(stream);
/*
             // 打印當前 Activity 的所有父類
            CtClass tmpSuper = ctCls.superclass
            while (tmpSuper != null) {
                println(tmpSuper.name)
                tmpSuper = tmpSuper.superclass
            }
*/
            // ctCls 之前的父類
            def originSuperCls = ctCls.superclass

            /* 從當前 Activity 往上回溯尤辱,直到找到需要替換的 Activity */
            def superCls = originSuperCls
            while (superCls != null && !(superCls.name in loaderActivityRules.keySet())) {
                // println ">>> 向上查找 $superCls.name"
                ctCls = superCls
                superCls = ctCls.superclass
            }

            // 如果 ctCls 已經(jīng)是 LoaderActivity砂豌,則不修改
            if (ctCls.name in loaderActivityRules.values()) {
                // println "    跳過 ${ctCls.getName()}"
                return
            }

            /* 找到需要替換的 Activity, 修改 Activity 的父類為 LoaderActivity */
            if (superCls != null) {
                def targetSuperClsName = loaderActivityRules.get(superCls.name)
                // println "    ${ctCls.getName()} 的父類 $superCls.name 需要替換為 ${targetSuperClsName}"
                CtClass targetSuperCls = pool.get(targetSuperClsName)

                if (ctCls.isFrozen()) {
                    ctCls.defrost()
                }
                ctCls.setSuperclass(targetSuperCls)

                // 修改聲明的父類后,還需要方法中所有的 super 調(diào)用光督。
                ctCls.getDeclaredMethods().each { outerMethod ->
                    outerMethod.instrument(new ExprEditor() {
                        @Override
                        void edit(MethodCall call) throws CannotCompileException {
                            if (call.isSuper()) {
                                if (call.getMethod().getReturnType().getName() == 'void') {
                                    call.replace('{super.' + call.getMethodName() + '($$);}')
                                } else {
                                    call.replace('{$_ = super.' + call.getMethodName() + '($$);}')
                                }
                            }
                        }
                    })
                }

                ctCls.writeFile(CommonData.getClassPath(ctCls.name))
                println "    Replace ${ctCls.name}'s SuperClass ${superCls.name} to ${targetSuperCls.name}"
            }

        } catch (Throwable t) {
            println "    [Warning] --> ${t.toString()}"
        } finally {
            if (ctCls != null) {
                ctCls.detach()
            }
            if (stream != null) {
                stream.close()
            }
        }
    }

2.4.7將解壓修改后的jar重新打包阳距,然后刪除解壓目錄,只留jar结借。源碼如下

    /**
     * 將解壓的 class 文件重新打包筐摘,然后刪除 class 文件
     */
    def repackage() {
        Util.newSection()
        println '>>> Repackage...'
        includeJars.each {
            File jar = new File(it)
            String JarAfterzip = map.get(jar.getParent() + File.separatorChar + jar.getName())
            String dirAfterUnzip = JarAfterzip.replace('.jar', '')
            // println ">>> 壓縮目錄 $dirAfterUnzip"

            Util.zipDir(dirAfterUnzip, JarAfterzip)

            // println ">>> 刪除目錄 $dirAfterUnzip"
            FileUtils.deleteDirectory(new File(dirAfterUnzip))
        }
    }

2.4.8最后將處理好的class和jar賦值到輸出目錄,jar改名避免重名船老,交給下個task處理咖熟,至此Transform結(jié)束。

 /**
     * 拷貝處理結(jié)果
     */
    def copyResult(def inputs, def outputs) {
        // Util.newSection()
        inputs.each { TransformInput input ->
            input.directoryInputs.each { DirectoryInput dirInput ->
                copyDir(outputs, dirInput)
            }
            input.jarInputs.each { JarInput jarInput ->
                copyJar(outputs, jarInput)
            }
        }
    }

    /**
     * 拷貝目錄
     */
    def copyDir(TransformOutputProvider output, DirectoryInput input) {
        File dest = output.getContentLocation(input.name, input.contentTypes, input.scopes, Format.DIRECTORY)
        FileUtils.copyDirectory(input.file, dest)
//        println ">>> 拷貝目錄 ${input.file.absolutePath} 到 ${dest.absolutePath}"
    }
    /**
     * 拷貝 Jar
     */
    def copyJar(TransformOutputProvider output, JarInput input) {
        File jar = input.file
        String jarPath = map.get(jar.absolutePath);
        if (jarPath != null) {
            jar = new File(jarPath)
        }

        String destName = input.name
        def hexName = DigestUtils.md5Hex(jar.absolutePath)
        if (destName.endsWith('.jar')) {
            destName = destName.substring(0, destName.length() - 4)
        }
        File dest = output.getContentLocation(destName + '_' + hexName, input.contentTypes, input.scopes, Format.JAR)
        FileUtils.copyFile(jar, dest)

/*
        def path = jar.absolutePath
        if (path in CommonData.includeJars) {
            println ">>> 拷貝Jar ${path} 到 ${dest.absolutePath}"
        }
*/
    }
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末柳畔,一起剝皮案震驚了整個濱河市馍管,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌薪韩,老刑警劉巖确沸,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件捌锭,死亡現(xiàn)場離奇詭異,居然都是意外死亡罗捎,警方通過查閱死者的電腦和手機观谦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來桨菜,“玉大人豁状,你說我怎么就攤上這事〉沟茫” “怎么了替蔬?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長屎暇。 經(jīng)常有香客問我,道長驻粟,這世上最難降的妖魔是什么根悼? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮蜀撑,結(jié)果婚禮上挤巡,老公的妹妹穿的比我還像新娘。我一直安慰自己酷麦,他們只是感情好矿卑,可當我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著沃饶,像睡著了一般母廷。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上糊肤,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天琴昆,我揣著相機與錄音,去河邊找鬼馆揉。 笑死业舍,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的升酣。 我是一名探鬼主播舷暮,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼噩茄!你這毒婦竟也來了下面?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤巢墅,失蹤者是張志新(化名)和其女友劉穎诸狭,沒想到半個月后券膀,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡驯遇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年芹彬,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片叉庐。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡舒帮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出陡叠,到底是詐尸還是另有隱情玩郊,我是刑警寧澤,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布枉阵,位于F島的核電站译红,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏兴溜。R本人自食惡果不足惜侦厚,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望拙徽。 院中可真熱鬧刨沦,春花似錦、人聲如沸膘怕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽岛心。三九已至来破,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間忘古,已是汗流浹背讳癌。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留存皂,地道東北人晌坤。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像旦袋,于是被迫代替她去往敵國和親骤菠。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,927評論 2 355

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