replugin源碼解析之replugin-host-gradle(宿主的gradle插件)

前言

replugin-host-gradle 是 RePlugin 插件框架中的宿主gradle插件,主要用于在宿主應用的編譯期常規構建任務流中,插入一些定制化的構建任務,以便實現自動化編譯期修改宿主應用的目的。
RePlugin 是一套完整的、穩定的、適合全面使用的,占坑類插件化方案,由360手機衛士的RePlugin Team研發,也是業內首個提出”全面插件化“(全面特性、全面兼容、全面使用)的方案。

注:文中會提及兩種插件,請閱讀本文時注意提及插件的上下文情景,避免混淆概念:

  • replugin插件:即replugin插件化框架所指的插件,這個插件指android應用業務拆分出的獨立模塊,是android應用或模塊。
  • gradle插件:即gradle構建所需的構建插件,是gradle應用或模塊。

結構概覽

結構概覽 - 英文高清大圖 ------------------ 結構概覽 - 中文高清大圖

replugin-host-gradle,針對宿主應用執行的構建任務:

  • 生成帶 RePlugin 插件坑位的 AndroidManifest.xml(允許自定義數量)
  • 生成 RepluginHostConfig 類,方便插件框架讀取并自定義其屬性
  • 生成 plugins-builtin.json,json中含有插件應用的信息,包名,插件名,插件路徑等。

replugin-host-gradle 插件的構建任務基于{productFlavors}{buildTypes}組合出多維構建任務,在android gradle 插件構建規則內執行構建任務,舉個具體的例子:
在宿主中配置了 兩個渠道{baidu} {xiaomi},兩個編譯類型{debug} {release}
共會生成四種編譯組合:
{baidu}{debug} {xiaomi}{debug} {baidu}{release} {xiaomi}{release}
每種組合都會執行經由replugin-host-gradle 插件插入或修改到默認構建任務流中的gradle task為:
rpGenerate{productFlavors}{buildTypes}HostConfig - 生成RePluginHostConfig.java配置文件到buildConfig目錄下
process{productFlavors}{buildTypes}Manifest - 拼裝生成 AndroidManifest.xml(坑位組件+原xml中的組件)
rpGenerate{productFlavors}{buildTypes}BuiltinJson - 生成插件信息文件plugins-builtin.json到assets目錄下

目錄概覽

\qihoo\RePlugin\replugin-host-gradle\src
│
└─main
    ├─groovy
    │  └─com
    │      └─qihoo360
    │          └─replugin
    │              └─gradle
    │                  └─host
    │                      │  AppConstant.groovy                    # 程序常量定義區
    │                      │  RePlugin.groovy                      # 針對宿主的特定構建任務創建及調度
    │                      │  
    │                      ├─creator
    │                      │  │  FileCreators.groovy                # 組裝生成器
    │                      │  │  IFileCreator.groovy                # 文件生成器接口
    │                      │  │  
    │                      │  └─impl
    │                      │      ├─java
    │                      │      │      RePluginHostConfigCreator.groovy       # RePluginHostConfig.java 生成器
    │                      │      │      
    │                      │      └─json
    │                      │              PluginBuiltinJsonCreator.groovy       # plugins-builtin.json 生成器
    │                      │              PluginInfo.groovy                   # 插件信息模型
    │                      │              PluginInfoParser.groovy               # 從 manifest 的 xml 中抽取 PluginInfo信息
    │                      │              
    │                      └─handlemanifest
    │                              ComponentsGenerator.groovy       # 動態生成插件化框架中需要的組件
    │                              
    └─resources
        └─META-INF
            └─gradle-plugins
                    replugin-host-gradle.properties               # 指定 gradle 插件實現類

replugin-host-gradle的基本用法

  • 添加 RePlugin Host Gradle 依賴
    在項目根目錄的 build.gradle(注意:不是 app/build.gradle) 中添加 replugin-host-gradle 依賴:
buildscript {
    dependencies {
        classpath 'com.qihoo360.replugin:replugin-host-gradle:2.1.5'
        ...
    }
}

在項目的app模塊中的build.gradle應用插件:

apply plugin: 'replugin-host-gradle'

replugin-host-gradle的源碼解析

我們在開始閱讀源碼前,要思考下,replugin-host-gradle是什么?
A:replugin-host-gradle是一個自定義的gradle插件。
這個清楚了,那就上車吧。

講解replugin-host-gradle源碼的同時,還會講解一些開發自定義gradle插件的知識,希望能和您一起:知其然,亦知其所以然。

replugin-host-gradle.properties文件

implementation-class=com.qihoo360.replugin.gradle.host.Replugin

在開發自定義gradle插件時,都會先定義這么個文件。這里有 2 個知識點:

  • 文件中的implementation-class用來指定插件實現類。
  • 文件名用來指定插件名,即在宿主中使用插件時的apply plugin: 'replugin-host-gradle'中的replugin-host-gradle.

我們到插件實現類看看這個插件是如何工作的。

此 gradle 插件基于 groovy 開發,groovy 也是 JVM 系的編程語言,對于 java 系程序員來說,幾乎可以閉著眼就開擼代碼,不過 gradle 基于 Groovy,build 腳本使用 Groovy 編寫,想寫出 gradle style 的代碼,還是可以去學學這門語言。

RePlugin.groovy文件

public class Replugin implements Plugin<Project> {
    @Override
        public void apply(Project project) {
            println "${TAG} Welcome to replugin world ! "
            ...
    }
}

定義了一個類RePlugin,繼承自gradle-api 庫中的接口類 Plugin<Project> ,實現了apply接口方法,apply方法會在 build.gradle 中執行 apply plugin: 'replugin-host-gradle' 時被調用。

那我們分小節,循序漸進的看看 apply 方法的具體實現。

預生成AndroidManifest.xml中的組件坑位

@Override
    public void apply(Project project) {

        println "${TAG} Welcome to replugin world ! "

        this.project = project

        /* Extensions */
        project.extensions.create(AppConstant.USER_CONFIG, RepluginConfig)

        if (project.plugins.hasPlugin(AppPlugin)) {

            def android = project.extensions.getByType(AppExtension)
            android.applicationVariants.all { variant ->

                addShowPluginTask(variant)

                if (config == null) {
                    config = project.extensions.getByName(AppConstant.USER_CONFIG)
                    checkUserConfig(config)
                }

                def appID = variant.generateBuildConfig.appPackageName
                println "${TAG} appID: ${appID}"
                def newManifest = ComponentsGenerator.generateComponent(appID, config)
 
                ...
            }
        }
    }
  • 首先向Plugin傳遞參數,通過project.extensions.create(AppConstant.USER_CONFIG, RepluginConfig),將RepluginConfig類的常量配置信息賦值給AppConstant.USER_CONFIG,在接下來checkUserConfig(config)檢查配置信息時有用到,主要檢查配置信息數據類型是否正確。
  • 判斷project中是否含有AppPlugin類型插件,即是否有'application' projects類型的Gradle plugin。我們在宿主項目中是應用了該類型插件的:apply plugin: 'com.android.application'.
    如果希望判斷是否有libraryPlugin,可以這樣寫:if (project.plugins.hasPlugin(LibraryPlugin)),it's for 'library' projects.
  • 獲取project中的AppExtension類型extension,即com.android.application projects的android extension.也就是在你的app模塊的build.gradle中定義的閉包:
android {
    ...
}

遍歷android extension的Application variants 列表。這里說下,這可以說是 Hook Android gradle 插件的一種方式,因為通過遍歷applicationVariants,你可以修改屬性,名字,描述,輸出文件名等,如果是Android library庫,那么就將applicationVariants替換為libraryVariants。很多人可能在build.gradle中這樣定義過閉包:

buildTypes {
        release {
            applicationVariants.all { variant ->
                variant.outputs.each { output ->
                    def outputFile = output.outputFile
                    def fileName = "xxx_${variant.productFlavors[0].name}_v${defaultConfig.versionName}_${releaseTime()}.apk"
                    output.outputFile = new File(outputFile.parent, fileName)
                }
            }
        }
    }

其實這也是一種插件的創建方式,Hook Android gradle 插件動態修改variants屬性值,修改打包輸出的apk文件名。
創建自定義gradle插件,Gradle提供了多種方式:

  • 在build.gradle腳本中直接創建(上述代碼即是)
  • 在獨立Module中創建(replugin-host-gradle即是)
  • 繼續看代碼,addShowPluginTask(variant)這個方法執行了,但是方法內指定的task并未掛到android gradle task上,即task不會執行。這個task是方便調試時查看插件信息的,任務內容同接下來將講到的生成 plugins-builtin.json 插件信息文件task一致。
  • checkUserConfig(config),獲取到AppConstant.USER_CONFIG內一系列參數后,做數據類型正確性校驗。
  • 關鍵代碼來了,下面一行代碼,搞定了宿主中AndroidManifest.xml中的組件坑位生成,注意,結合結構概覽中的gradle Flow 看,這里只是生成組件坑位的xml代碼,最終的xml文件是在后續的task中拼裝出來的,稍后會講到。
def newManifest = ComponentsGenerator.generateComponent(appID, config)

在代碼面前,一切都是紙老虎。上車,進去看如何生成坑位的。

``` groovy
def static generateComponent(def applicationID, def config) {
        // 是否使用 AppCompat 庫(涉及到默認主題)
        if (config.useAppCompat) {
            themeNTS = THEME_NTS_NOT_APP_COMPAT
        } else {
            themeNTS = THEME_NTS_NOT_USE_APP_COMPAT
        }

        def writer = new StringWriter()
        def xml = new MarkupBuilder(writer)

        /* UI 進程 */
        xml.application {
            /* 透明坑 */
            config.countTranslucentStandard.times {
                activity(
                        "${name}": "${applicationID}.${infix}N1NRTS${it}",
                        "${cfg}": "${cfgV}",
                        "${exp}": "${expV}",
                        "${ori}": "${oriV}",
                        "${theme}": "${themeTS}")
                ...
            }

            ...

            /* 不透明坑 */
            config.countNotTranslucentStandard.times{

            }
            ...

        }
        // 刪除 application 標簽
        def normalStr = writer.toString().replace("<application>", "").replace("</application>", "")

//        println "${TAG} normalStr: ${normalStr}"

        // 將單進程和多進程的組件相加
        normalStr + generateMultiProcessComponent(applicationID, config)
    }
```

一定要用一句話總結的話,那就是:基于 Groovy 的 MarkupBuilder api,根據 RepluginConfig 類中的配置,拼出組件坑位的xml 字符串。
就像搭積木一樣,看一組就明白了。
生成坑位的代碼:

config.countTranslucentStandard.times {
                activity(
                        "${name}": "${applicationID}.${infix}N1NRTS${it}",
                        "${cfg}": "${cfgV}",
                        "${exp}": "${expV}",
                        "${ori}": "${oriV}",
                        "${theme}": "${themeTS}")
            }

注:config.countTranslucentStandard.times 含義:根據config.countTranslucentStandard的值循環
生成的坑位:

<activity
            android:theme="@ref/0x01030010"
            android:name="com.qihoo360.replugin.sample.host.loader.a.ActivityN1NRTS0"
            android:exported="false"
            android:screenOrientation="1"
            android:configChanges="0x4b0" />

一個字總結:replace.

Tips. 可以用Android Studio的Analyze APK...功能查看host gradle插件構建后宿主的AndroidManifest.xml,看看生成的坑位的樣子就明白了。

生成 RePluginHostConfig 配置文件

@Override
    public void apply(Project project) {

       ...

        if (project.plugins.hasPlugin(AppPlugin)) {

            def android = project.extensions.getByType(AppExtension)
            android.applicationVariants.all { variant ->

                ...

                def variantData = variant.variantData
                def scope = variantData.scope

                //host generate task
                def generateHostConfigTaskName = scope.getTaskName(AppConstant.TASK_GENERATE, "HostConfig")
                def generateHostConfigTask = project.task(generateHostConfigTaskName)

                generateHostConfigTask.doLast {
                    FileCreators.createHostConfig(project, variant, config)
                }
                generateHostConfigTask.group = AppConstant.TASKS_GROUP

                //depends on build config task
                String generateBuildConfigTaskName = variant.getVariantData().getScope().getGenerateBuildConfigTask().name
                def generateBuildConfigTask = project.tasks.getByName(generateBuildConfigTaskName)
                if (generateBuildConfigTask) {
                    generateHostConfigTask.dependsOn generateBuildConfigTask
                    generateBuildConfigTask.finalizedBy generateHostConfigTask
                }

                ...

            }
        }
    }

繼續回到 apply 方法,接下來該到生成 RePluginHostConfig 的時候了,即 注釋中的host generate task

  • 首先生成了 HostConfig 的gradle task 名字,并調用project的task()方法創建此Task。
  • 指定了 generateHostConfigTask 的task任務:自動創建RePluginHostConfig.java至BuildConfig目錄。
    generateHostConfigTask.doLast {
        FileCreators.createHostConfig(project, variant, config)
    }

注:createHostConfig(...)方法內的實現,也是根據配置類 RepluginConfig中的配置信息拼裝生成的java文件。

  • 設置generateHostConfigTask的執行依賴
//depends on build config task
if (generateBuildConfigTask) {
    generateHostConfigTask.dependsOn generateBuildConfigTask
    generateBuildConfigTask.finalizedBy generateHostConfigTask
}

因為此task中創建的RePluginHostConfig.java希望放置到編譯輸出目錄..\replugin-sample\host\app\build\generated\source\buildConfig\{productFlavors}\{buildTypes}\...下,所以此task依賴于生成 BuildConfig.java 的task并設置為 BuildConfigTask 執行完后,就執行HostConfigTask。
關于gradle 的 task 相關知識,可以去gradle 官網或某搜索引擎查看學習,屬于字典型知識點,需要時候查閱下。

生成 plugins-builtin.json 插件信息文件

@Override
    public void apply(Project project) {

       ...

        if (project.plugins.hasPlugin(AppPlugin)) {

            def android = project.extensions.getByType(AppExtension)
            android.applicationVariants.all { variant ->

                ...

                //json generate task
                def generateBuiltinJsonTaskName = scope.getTaskName(AppConstant.TASK_GENERATE, "BuiltinJson")
                def generateBuiltinJsonTask = project.task(generateBuiltinJsonTaskName)

                generateBuiltinJsonTask.doLast {
                    FileCreators.createBuiltinJson(project, variant, config)
                }
                generateBuiltinJsonTask.group = AppConstant.TASKS_GROUP

                //depends on mergeAssets Task
                String mergeAssetsTaskName = variant.getVariantData().getScope().getMergeAssetsTask().name
                def mergeAssetsTask = project.tasks.getByName(mergeAssetsTaskName)
                if (mergeAssetsTask) {
                    generateBuiltinJsonTask.dependsOn mergeAssetsTask
                    mergeAssetsTask.finalizedBy generateBuiltinJsonTask
                }

                ...

            }
        }
    }

繼續回到 apply 方法,接下來該到生成 plugins-builtin.json 這個包含了插件信息的文件的時候了,即 注釋中的json generate task

  • 首先生成個gradle task 名字,并調用project的task()方法創建此Task。
  • 指定了 generateBuiltinJsonTask 的task任務:掃描宿主\assets\plugins目錄下的插件文件,并基于apk文件規則解析出插件信息,包名,版本號等,然后拼裝成json文件。
generateBuiltinJsonTask.doLast {
    FileCreators.createBuiltinJson(project, variant, config)
}
  • 設置 generateBuiltinJsonTask 的執行依賴
//depends on build config task
if (mergeAssetsTask) {
    generateBuiltinJsonTask.dependsOn mergeAssetsTask
    mergeAssetsTask.finalizedBy generateBuiltinJsonTask
}

因為此task中創建的 plugins-builtin.json 希望放置到編譯輸出目錄...\replugin-sample\host\app\build\intermediates\assets\{productFlavors}\{buildTypes}\...下,所以此task依賴于merge assets文件 的task并設置為 mergeAssetsTask 執行完后,就執行BuiltinJsonTask。

拼裝 AndroidManifest.xml

output.processManifest.doLast {
    def manifestPath = output.processManifest.outputFile.absolutePath
    def updatedContent = new File(manifestPath).getText("UTF-8").replaceAll("</application>", newManifest + "</application>")
    new File(manifestPath).write(updatedContent, 'UTF-8')
}
  • 將坑位 xml 字符串 與 原有xml <application></application> 標簽內的配置信息合二為一。

至此,replugin-host-gradle 插件的工作就全部結束了。

End

replugin-host-gradle 插件是一個compile-time gradle plugin,基于賦予android gradle 構建任務流中新的構建任務及修改已有的構建任務,進而實現動態修改構建目標文件的為replugin宿主服務的gradle插件。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容