在开始本节内容前,先来温习下几个关键词:
- Project(项目) → Gradle的构建 → 由一个或多个Project组成;
- Task(任务) → Gradle中的Project → 由一个或多个Task组成;
- Action(执行动作) → Gradle中的Task → 由一个或多个Action(函数/方法)按序组成;
然后再康康上一节提到的Gradle构建生命周期:
上图中有三处提到了 Script,在Gradle中,他们是 配置脚本,脚本在执行时,实际上是配置了一个特殊类型的对象:
Init Script → Gradle对象、Settings Script → Settings对象、Build Script → Project对象;
这个对象又称脚本的 代理对象,代理对象上的每个属性、方法都可以在脚本中使用。
每个Gradle脚本都实现了Script接口,由0个或多个 脚本语句 (statements)和 脚本块 组成。 脚本语句可以包含:函数调用、属性赋值和本地变量定义,脚本块则是一个方法调用,传入一个 配置闭包,执行时对代理对象进行配置。
本节就来细说下 构建生命周期 及 依赖规则&依赖冲突处理,这部分内容比较枯燥,但我依旧会写得简洁易懂些,希望看完对你解决Android多模块构建问题时可以有所裨益。
0x1、Initialization(初始化)
1. Init Script(初始化脚本)
涉及到的文件及脚本执行顺序如下:
$GRADLE_USER_HOME/init.gradle(.kts) → $GRADLE_USER_HOME/init.d/[*.gradle(.kts)]
这一步会生成 Gradle 对象,提供了三类API:获取全局属性、项目配置、生命周期HOOK,部分API如下:
// 获得Gradle实例的方法:在*.gradle文件中调用.gradle 或 Project.getGradle()。 /* ========= ① 获取全局属性 ========= */ gradleHomeDir → 执行此次构建的Gradle目录; gradleUserHomeDir → Gradle User Home目录; gradleVersion → 当前Gradle版本; includedBuilds → 获取内嵌构建; parent → 获取父构建; pluginManager → 获取插件管理器实例; plugins → 获取插件容器; rootProject → 获取当前构建的根项目; startParameter → 获取传入当前构建的所有参数 taskGraph → 获取当前构建的task graph,此对象在taskGraph.whenReady { } 后才具有内容 /* ========= ② 项目配置,闭包方法会在Project可用时立即执行 ========= */ rootProject(action) // 为Root Project添加闭包 allprojects(action) // 为所有 Project添加闭包
应用示例:Gradle全局设置Maven仓库,创建一个 $GRADLE_USER_HOME/init.gradle(.kts) 或在 $GRADLE_USER_HOME/init.d/ 目录下随便创建一个xxx.gradle(.ktx)文件,内容如下:
// 项目依赖仓库 allprojects { repositories { maven { url "https://maven.aliyun.com/repository/google" } maven { url "https://maven.aliyun.com/repository/jcenter" } } } // Gradle脚本依赖仓库 gradle.projectsLoaded { rootProject.buildscript { repositories { maven { url "https://maven.aliyun.com/repository/google" } maven { url "https://maven.aliyun.com/repository/jcenter" } } } }
配置后,Gradle项目构建时会优先从这里的Maven仓库下载依赖,然后再到项目中配置的仓库中下载,在Gradle编译提速中,可把Maven地址替换为自己搭建的Maven私服,所以Gradle项目编译时都会优先走这里~
2. Settings Script(设置脚本)
涉及文件:项目根目录下的 settings.gradle(.kts),在此文件中:声明参数构建的模块 及 管理构建过程需要的插件,此处会生成一个 Settings 对象。
Gradle会从当前目录开始查找此文件,找到停止找不到则往父目录递归查找,所以建议不管是单项目还是多项目,都要有一个 settings.gradle(.kts) 文件。
① 声明参数构建的模块
Settings类中,最重要的方法就是 include(String… projectPaths) 方法,用于添加参与构建的Project,传入一个 可变参数,值是每个Project的路径( 当前project相对于根project的路径 ),示例如下:
// [:]代表项目分隔符,类似于路径分隔中的[/],以:开头表示相对于根目录 include ':module1' include ':libs:library1' // 也可写到一行 include ':module1',':libs:library1' // 注:当子项目不在根目录下时需使用相对路径描述 project(":module3").projectDir = File(rootDir, "../../library2") // 默认情况下Gradle会使用根项目所在目录名称作为项目名 // 配合CI一起使用时,往往会检测到一个随机文件名,可以强制指定项目名称 rootProject.name = 'JustTest'
每个被include的项目都会生成 ProjectDescriptor 对象, 用于描述该模块。模块名称最终都会添加到Map类型的 DefaultProjectRegistry.projects 中,所以无需特殊处理include的顺序。
另外,即便settings.gradle(.kts)什么都不写,也会加载当前目录下的Build Script。
② 管理构建过程需要的插件
通过 settings.pluginManagement 的相关接口实现,比如指定插件的仓库地址(默认从Gradle官方创建仓库查找),打开settings.gradle:
pluginManagement { // 对应PluginManagementSpec类 repositories { // 管理Plugin Repository(仓库) google { url "https://maven.aliyun.com/repository/gradle-plugin" } } } rootProject.name = 'temp' include ':module1',':module2'
利用 resolutionStrategy 接口则可进行插件决策,比如打印一个Kotlin项目用到的插件信息:
resolutionStrategy { eachPlugin { // 接收一个PluginResolveDetails类型的闭包,通过requestsd可以获得plugin的信息 println "${requested.id} → ${requested.module} → ${requested.version}" } }
输出结果如下:
接着可以根据id替换插件或指定插件版本,示例如下:
resolutionStrategy { eachPlugin { // 接收一个PluginResolveDetails类型的闭包,requested可以获得plugin的信息 println "${requested.id} → ${requested.module} → ${requested.version}" // 替换模块 if (requested.id.namespace == "org.jetbrains.kotlin") { useModule("org.jetbrains.kotlin.jvm:org.jetbrains.kotlin.jvm.gradle.plugin:${requested.version}") } // 统一插件版本 if (requested.id.id == "org.jetbrains.kotlin.jvm") { useVersion("1.3.71") } } }
另外,此阶段涉及到的两个生命周期事件:settingsEvaluated() 和 projectLoaded(),前者可以拿到配置完毕的 Setting 对象,后者可以拿到包含项目基础信息的 Project 对象。
3. Build Script(构建脚本)
涉及文件:模块目录下的 build.gradle(.kts),用于配置当前模块的 构建信息,分为:
- 根目录模块的
Root Build Script(一般是对子模块进行统一的配置,没有太多内容);
- 子模块的
Module Build Script;
多模块的构建流程:Init Script → Settings Script → Root Build Script(单模块没这一步) → Build Script (默认字母序,可通过设置依赖关系干预)
Build Script完成的工作有两个:插件引入 和 属性配置,即对 Project 对象进行进一步的配置,生成Task的有向无环图。
① 插件引入
Gradle自身 并没有提供编译打包的功能,它只是一个 负责定义流程和规则的框架,具体的编译工作都是由 插件 来完成的,比如编译Java用Java插件,编译Kotlin用Kotlin插件。
所以插件到底是什么?→ 答:定义Task,并具体执行这些Task的模板。
插件的两种类型:
- 脚本插件:存在于另一个脚本文件中的一段脚本代码;
- 二进制插件(编译成字节码):实现Plugin接口,通过编程的方式操作构建过程(项目或Jar包形式);
Gradle会内置一些核心插件,并提供简单名字,如 "java",没在其中的插件则需采用完整名字,如:"org.jetbrains.kotlin:kotlin-gradle-plugin",这个又称插件id,唯一不可重复!引入方式区别如下:
// 内置插件引入 apply plugin: 'kotlin-android' // 也可以使用plugins,不过有些插件不能指定版本,有些必须指定,要注意! // 下面这种写法是Kotlin中的中缀表达式,apply→ 是否立即应用插件 plugins { id("org.jetbrains.kotlin.jvm") version "1.3.71" id("org.jetbrains.kotlin.jvm") version "1.3.71" apply false java `build-scan` } // 非内置插件引入,会将对应Jar文件放到Gradle的classpath下 buildscript { repositories { jcenter() } dependencies { classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.72" } }
② 属性配置
一旦应用了某个插件,就可以使用此插件提供的DSL进行配置,以此干预模块的构建过程。以Android构建为例:
// 引入android.application插件 → 为Project对象添加一个android{}配置闭包 apply plugin: 'com.android.application' android { compileSdkVersion 29 // 使用API 29编译此模块 // 编译时的一些配置 defaultConfig { applicationId "com.example.test" minSdkVersion 26 targetSdkVersion 29 versionCode 1 versionName "1.0" } // 签名配置 signingConfigs { release { storeFile file('test.jks') storePassword '123456' keyAlias 'test' keyPassword '123456' } } // 构建类型配置 buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' signingConfig signingConfigs.release } } // 编译选项配置 compileOptions{ sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } }
除了插件另外引入的属性DSL外,Project对象也提供了很多用于配置构建的DSL,如 dependencies 配置编译依赖项,更多可以点进Project源码中自行查看。
另外根目录Build Script还可以使用一个 ext 属性用于Project间的数据共享、统一模块依赖版本。
// 根目录build.gradle配置 ext { applicationId = "xxx.xxx.xxx" buildToolsVersion = "28.0.3" compileSdkVersion = 28 minSdkVersion = 22 ... } // 子模块build.gradle使用 android { compileSdkVersion rootProject.ext.compileSdkVersion buildToolsVersion rootProject.ext.buildToolsVersion ... }