前言
老Android都经历过从Eclipse切换到AS后对groovy脚本的适应期,groovy语言早期有效的参考资料很少; 不支持类似Java一样可以方便的查看对应方法的实现。大约从三年前的4.10版本开始支持Kotlin编写,一定程度上解放了使用groovy 编写带来的懵逼;本文讲实战,不聊深入的八股原理,下图为apk构建的简略流程图。
1、gradle插件的架子
1.1、 apply plugin: ‘sunflower’后面的插件名称sunflower是怎么定义的。
如下图,sunflower就是resource下面的sunflower.properties的名字,不带后缀。
此文件支持多份,比如:
可以复制一份文件命名为sunflowe_android.properties;那么工程中可以使用:
apply plugin: ‘sunflowe_android’
1.2、 classpath “com.xxx.plugin:sunflower:${project.properties[“sunflower.version”]}”是怎么定义的
其实和日常开发上传的aar组件版本格式一样,gradle的plugin插件也是通过和aar一样的格式上传的;
都是通过groupId:artifactId:version的格式上传插件的静态库。
apply plugin: 'maven'
apply plugin: 'maven-publish'
// nexus.properties文件的读取
Properties config = new Properties()
config.load(project.file("nexus.properties").newDataInputStream())
def nexus_versionName = config.getProperty('nexus_versionName')
。。。
uploadArchives {
repositories {
mavenDeployer {
pom.groupId = nexus_groupId
pom.artifactId = nexus_artifactId
pom.packaging = 'aar'
pom.name = nexus_fileName
pom.version = nexus_versionName
。。。
repository(url: url) {
authentication(userName: System.getenv("maven_username"), password: System.getenv("maven_password"))
}
}
}
}
1.3、 插件源码的工程入口;
工程入口以类的全路径形式定义在resource目录下的sunflower.properties中:
implementation-class=com.xxx.plugin.sunflower.SunflowerPlugin
// 范型注意使用PluginAware以区分Setting还是Project
class SunflowerPlugin: Plugin<PluginAware> {
private var plugin: SFPlugin? = null
override fun apply(pluginAware: PluginAware) {
plugin = PluginFactory.createPlugin(pluginAware)
。。。
plugin?.apply()
}
}
// 这里主要区分Settings中插件的应用为了源码和aar的切换,开发时调试用的
object PluginFactory {
fun createPlugin(pluginAware: PluginAware): SFPlugin? {
return when(pluginAware) {
is Settings -> SettingsPlugin(pluginAware)
is Project -> {
when(pluginAware.rootProject) {
pluginAware -> {
RootProjectPlugin(pluginAware)
}
else -> SubProjectPlugin(pluginAware)
}
}
else -> null
}
}
}
2、强制依赖和源码切换
2.1、强制依赖实现;
在gitlab的仓库直接定义文件3rdDep.gradle,格式如下
ext{
butterknifeVersion = '10.2.0'
kotlinVersion = '1.3.61'
dep = [
android_tools: "com.android.tools.build:gradle:3.2.1",
walle : "com.meituan.android.walle:plugin:1.1.6",
butterknife : "com.jakewharton:butterknife-gradle-plugin:${butterknifeVersion}",
kotlinPlugin : "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}"
]
}
创建一个Task
class XXXPlugin : Plugin<PluginAware> {
override fun apply(pluginAware: PluginAware) {
project = PluginFactory.createPlugin(pluginAware)
...
// 当pluginAware是Project的时候
val task = project.tasks.create("DepDefinitionPluginTask", ApplyHbRemoteDepTask::class.java)
project.afterEvaluate() {
task.apply()
}
}
}
task实现强制依赖
open class ApplyHbRemoteDepTask : DefaultTask() {
@TaskAction
fun apply() {
...
depPluginAddress: String = "https://gitlab.hellobike.cn/Publics/NewAndroidBOSDepPlugin"
val url = "$depPluginAddress/raw/$curBranch/dep/3rdDep.gradle"
project.apply(mapOf(Pair("from", url)))
project.extensions.extraProperties.get("dep") as Map<*, *>?
project.subprojects {
val depModuleSelectorNotations = mutableListOf<String>()
collectDepModuleVersionSelectorNotations(depModuleSelectorNotations,
project.extensions.extraProperties.get("dep") as Map<*, *>?)
it.configurations.all { configuration ->
// 核心实现
configuration.resolutionStrategy.force(depModuleSelectorNotations)
}
}
}
}
private fun collectDepModuleVersionSelectorNotations(list: MutableList<String>, depConfig: Map<*, *>?) {
depConfig?.values?.forEach {
if (it is Map<*, *>) {
collectDepModuleVersionSelectorNotations(list, it)
} else if (it is String || it is GStringImpl) {
val notation: String = it.toString()
// println("[强制依赖]:" + notation)
list.add(notation)
}
}
}
2.2、反向依赖
2.1、工程根目录下定义一份切换aar和源码实现的文件repo.xml
<?xml version="1.0" encoding="UTF-8" ?>
<repo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.w3school.com.cn"
xsi:schemaLocation="http://www.w3school.com.cn repo.xsd">
<!--srcBuild:是否开启源码依赖,该配置优先级低于module中的srcBuild-->
<!--branch:
分支 用于在源码调试时统一分支,会自动进行分支切换,
如果有未暂存的文件修改会导致编译失败,并提示
该配置优先级 低于 module 配置中的 branch
-->
<default srcBuild="false"/>
<!--
origin:远程路径
substitute: 将被替换掉的远程依赖 group:name
name:唯一标示某一个模块,该属性名 也是 该模块在原仓库的默认文件夹名称
-->
<!--middle 层-->
<module name="middle-bundlelibrary"
origin="ssh://git@gitlab.xxx.cn:10022/Torrent/XXXAndroidMiddleApp.git"
srcBuild="false" substitute="com.xxx.middle:middle-bundlelibrary" />
</repo>
2.2、 Plugin入口处指定源码文件的配置信息
这里的实现需要在Setting文件进行:apply plugin: ‘com.xxx.repo-setting’
class RepoSettingsPlugin : Plugin<PluginAware> {
override fun apply(settings: PluginAware) {
// 当PluginAware是一个Setting的时候
settings = PluginFactory.createPlugin(pluginAware)
val repoInfo = RepoInflater.inflate(settings.rootDir)
repoInfo.moduleInfoMap.forEach { s, module ->
if (module.srcBuild ) {
// 执行一些git命中clone到指定的目录
module.settingProject()
// 源码工程囊括进来,就是常见的在Setting文件中include ':app'
settings.include(":${module.name}")
RepoLogger.info("${module.name} 路径为 ${module.modulePath}");
// 指定include工程的文件地址
settings.project(":${module.name}").projectDir = module.modulePath
}
}
}
}
// module的数据结构
open class ModuleInfo(
val name: String,
val origin: String,
val path: String?,
val srcBuild: Boolean,
val substitute: String?,
val repoManageProjectDir: File,
val branch: String
)
这里的实现需要在工程的build.gradle文件进行:apply plugin: ‘com.xxx.repo-setting’
class RepoSettingsPlugin:Plugin<PluginAware> {
override fun apply(project: PluginAware) {
// 当PluginAware是一个RootProject的时候
rootProject = PluginFactory.createPlugin(pluginAware)
val repoInfo = RepoInflater.inflate(project.rootProject.projectDir)
val substituteModule = repoInfo.substituteModules
rootProject.subprojects.forEach { target ->
target.afterEvaluate {
substituteModules.forEach { substituteModule ->
target.configurations.all{
it.dependencySubstitution.substitute(
// com.hellobike.middle:middle-bundlelibrary
it.dependencySubstitution.module(substituteModule.targetModule))
//替换成本地源码文件地址
.with(it.dependencySubstitution.project(substituteModule.project)) }
}
}
}
}
}
// RepoInfo的数据结构
class RepoInfo(val repoManageProjectDir:File) {
var moduleInfoMap: MutableMap<String, ModuleInfo> = mutableMapOf()
var substituteModules: List<SubstituteModule> = mutableListOf()
var defaultBranch: String? = null;
var srcBuild: Boolean = false;
}
3、部分省略操作的补充
3.1、包括cmd操作在内的git操作
class CmdUtil {
companion object {
fun execute(command: String): Boolean {
return execute(command, null)
}
fun execute(command: String,dir: File?):Boolean{
return execute(command, dir, true)
}
fun execute(command: String, dir: File?, interruptWhenCmdFailed: Boolean): Boolean {
// 核心逻辑
val process = Runtime.getRuntime().exec(command, null, dir)
val result = process.waitFor()
if (result != 0) {
val failureMsg = "[CMD] - failure to execute command [${command} under ${dir}\n message: ${process.errorStream.bufferedReader().readText()}"
if (interruptWhenCmdFailed) {
throw CmdExecuteException(command, failureMsg)
} else {
System.out.println(failureMsg)
}
} else {
System.out.println("【CMD】- execute command [${command}] under ${dir} success\n ")
}
return result == 0;
}
}
}
class GitUtil {
companion object {
//get Cur branch
fun curBranch(): String {
return CmdUtil.executeForOutput("git rev-parse --abbrev-ref HEAD", null, false).replace("\n", "")
}
fun curBranchName(dir: File): String {
return CmdUtil.executeForOutput("git rev-parse --abbrev-ref HEAD", dir).replace("\n", "")
}
fun clone(dir: File, gitUrl: String, branch: String): Boolean {
return CmdUtil.execute("git clone ${gitUrl} -l ${dir} -b ${branch}", null, true)
}
fun clone(dir: File, gitUrl: String): Boolean {
return CmdUtil.execute("git clone ${gitUrl} -l ${dir}", null, true)
}
fun slightClone(dir: File, gitUrl: String, branch: String, depth: Int): Boolean {
return CmdUtil.execute("git clone ${gitUrl} -l ${dir}", null, true)
}
fun isClean(dir: File): Boolean {
return CmdUtil.executeForOutput("git status -s", dir).trim() == "";
}
fun fileStatus(dir: File) :String{
return CmdUtil.executeForOutput("git status -s ", dir)
}
fun isLocalExistBranch(dir: File, branch: String): Boolean {
return File(dir, ".git/refs/heads/$branch").exists()
}
fun isBranchChanged(dir:File ,branch:String):Boolean {
return !curBranchName(dir).equals(branch)
}
fun checkoutBranch(dir: File, branch: String) {
CmdUtil.execute("git checkout ${branch}", dir)
}
fun checkoutRemoteBranch(dir: File, branch: String) {
CmdUtil.execute("git checkout -b $branch origin/$branch", dir)
}
fun checkoutNewBranch(dir: File, branch: String) {
CmdUtil.execute("git checkout -b $branch", dir)
}
fun isRemoteBranch(dir: File, branch: String): Boolean {
CmdUtil.execute("git fetch", dir)
val text = CmdUtil.executeForOutput("git branch -r",dir)
return text.contains("origin/$branch")
}
fun pullRebase(dir: File): Boolean {
return CmdUtil.execute("git pull --rebase", dir, false)
}
}
}
3.2、xml文件的解析
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
public RepoInfo parseRepo(File repoFile) throws ParserConfigurationException, IOException, SAXException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
FileInputStream inputStream = new FileInputStream(repoFile);
Document doc = builder.parse(inputStream);
Element rootElement = doc.getDocumentElement();
RepoInfo repoInfo = null;
NodeList defaultNodeList = rootElement.getElementsByTagName(TAG_DEFAULT);
if (defaultNodeList.getLength() > 1) {
throw new RuntimeException("[repo] - Make sure there is only one <default/> element in repo.xml");
} else if (defaultNodeList.getLength() == 1) {
Element item = (Element) defaultNodeList.item(0);
File repoDirPath = repoProjectsDefaultDir;
String branch = item.getAttribute(ATTRIBUTE_DEFAULT_BRANCH);
final boolean repoDirSetting = item.hasAttribute(ATTRIBUTE_REPO_DIR);
String repoDir = item.getAttribute(ATTRIBUTE_REPO_DIR);
if (repoDirSetting && repoDir != null) {
if (new File(repoDir).isAbsolute()) {
repoDirPath = new File(repoDir);
} else {
repoDirPath = new File(gradleProjectDir, repoDir);
}
}
//如果repo.xml 没有设置默认分支,则执行git命令获取当前分支名作为默认的分支
if (branch == null || branch.length() == 0) {
branch = GitUtil.Companion.curBranchName(gradleProjectDir);
}
//如果是临时分支,尝试获取jvm branch
//如果jvm branch配置还是没有则 默认取master
if ("HEAD".equals(branch)){
//
final String jvmBranch = System.getProperty("BRANCH");
if (jvmBranch ==null){
branch = "master";
}else {
if (jvmBranch.startsWith("origin/")){
branch = jvmBranch.replace("origin/", "");
}else {
branch = jvmBranch;
}
}
}
RepoLogger.Companion.info(" 设置默认分支" + branch);
repoInfo = new RepoInfo(repoDirPath);
repoInfo.setDefaultBranch(branch);
repoInfo.setSrcBuild(Boolean.parseBoolean(item.getAttribute(ATTRIBUTE_SRCBUILD)));
}
final NodeList moduleNodeList = rootElement.getElementsByTagName(TAG_MODULE);
for (int i = 0; i < moduleNodeList.getLength(); i++) {
final Element moduleElement = (Element) moduleNodeList.item(i);
parseModuleInfo(repoInfo, moduleElement);
}
final NodeList substituteNodeList = rootElement.getElementsByTagName(TAG_SUBSTITUTE);
for (int i = 0; i < substituteNodeList.getLength(); i++) {
final Element substituteModuleItem = (Element) substituteNodeList.item(i);
parseSubstituteModule(repoInfo, substituteModuleItem);
}
return repoInfo;
}
总结
- gradle插件其实结构较为简单,多编写几次一般能上手;
- 纸上得来终觉浅,觉知此事要躬行;不断思考coding是最好的实践;
- Android编译构建过程十分复杂宏大,有待深入挖掘。