前言
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插件。