前言
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插件。
网友评论
/* Extensions */
project.extensions.create(AppConstant.USER_CONFIG, RepluginConfig)
......
if (config == null) {
config = project.extensions.getByName(AppConstant.USER_CONFIG)
checkUserConfig(config)
}
我的理解是:应该将用户在build.gradle中自定义配置repluginHostConfig的内容,与默认的配置进行合并。但是没看到这样的逻辑实现。
com.qihoo360.replugin.sample.host.loader.a.ActivityN1NRTS0,这个坑位activity最终有对应的类吗。
大致上还看了plugin的gradle,它会替换Activity的继承为如PluginActivity,PluginActivity会内部做一些操作实现插件的打开。
但是坑位Activity怎么对应上呢?replugin只hook住了classLoader。startActivity启动的container是坑位。所以坑位Activity是怎么对应上目标的?
接下来我会写分析lib库的源码,到时候欢迎来阅读,你的疑问就会解决了