Gradle
Gradle是一种项目构建工具,说白了就是对项目进行配置,告诉编译器怎么编译项目的管理工具。面向Java应用为主。当前其支持的语言限于Java、Groovy、Kotlin和Scala,计划未来将支持更多的语言。
Gradle is an open-source build automation tool focused on flexibility and performance. Gradle build scripts are written using a Groovy or Kotlin DSL.
Gradle现在主要的呈现形式是Groovy语言,鉴于Google对Kotlin的大力推崇,和Kotlin更加灵活的特性(没有最灵活,只有更灵活),Gradle也支持用Kotlin编写,虽然,目前某些API的兼容还不是那么友好。
对于学生,比如我,Maven就够用了,除非Android强制的Gradle构建。但是在大型项目构建中,Gradle更加顺手,虽然我没做过啥大型项目😄。写这篇的目的是讲解Android中的build.gradle,方便能看懂。
背景
build.gradle的背景知识可以去官方文档,这里详细介绍了如何安装配置Gradle,以及如何通过gradle构建一个项目等等。
API
官方文档,这里详细介绍了Gradle的API,想要细致了解的也可以参考。
Gradle脚本
Gradle一般会产生两个脚本文件,settings.gradle和build.gradle。settings.gradle包含项目的模块信息,build.gradle包含项目(模块)的构建配置细节。每个脚本文件都对应一个实例对象,settings.gradle对应Setting,build.gradle对应Project。一个项目的构建过程遵循以下的生命周期:
- 创建Settings对象,默认配置;
- 要是有settings.gradle脚本,覆盖配置Settings对象;
- 根据Settings对象,建立Project实例们的层次性(比如多模块);
- 最后,基于宽度优先的原则,通过执行build.gradle配置构建每个Project,当然,可以通过
Project.evaluationDependsOnChildren()
或者Project.evaluationDependsOn(java.lang.String)
重载改变这种默认的构建顺序。
Gradle脚本由以下元素组成Plugins、Tasks、Deplendencies,Properties和Methods。
实例分析
apply plugin: 'com.android.application'
apply plugin: 'jacoco'
jacoco {
toolVersion = "0.8.5"
}
def localPropertiesFile = project.rootProject.file('signing.properties')
def useLocalSigning = localPropertiesFile.exists()
Properties localProperties = null
if (useLocalSigning) {
localProperties = new Properties()
localProperties.load(localPropertiesFile.newDataInputStream())
}
android {
compileSdkVersion 28
buildToolsVersion '28.0.3'
signingConfigs {
gbDistribution {
if (useLocalSigning) {
storeFile file(localProperties.getProperty('signing.storeFile'))
keyAlias localProperties.getProperty('signing.keyAlias')
storePassword localProperties.getProperty('signing.storePassword')
keyPassword localProperties.getProperty('signing.keyPassword')
}
}
}
defaultConfig {
applicationId "com.genonbeta.TrebleShot"
minSdkVersion 14
targetSdkVersion 28
versionCode 98
versionName "1.4.2"
testInstrumentationRunnerArguments clearPackageData: 'true'
vectorDrawables {
useSupportLibrary = true
}
}
lintOptions {
checkReleaseBuilds false
abortOnError false
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
testCoverageEnabled = true
applicationIdSuffix '.debug'
versionNameSuffix '-DEBUG'
}
}
flavorDimensions 'mode'
productFlavors {
fossReliant {
dimension 'mode'
signingConfig signingConfigs.gbDistribution
}
googlePlay {
dimension 'mode'
signingConfig signingConfigs.gbDistribution
}
}
}
task jacocoTestReport(type: JacocoReport) {
group = "Reporting"
description = "Generate Jacoco coverage reports after running tests."
reports {
xml.enabled = true
html.enabled = true
csv.enabled = true
}
classDirectories.from = fileTree(
dir: './build/intermediates/javac/fossReliantDebug/compileFossReliantDebugJavaWithJavac',
excludes: ['**/*$InjectAdapter.class',
'**/*$ModuleAdapter.class',
'**/*$ViewInjector*.class',
'**/R.class', '**/R$*.class'
])
sourceDirectories.from = files('../app/src/main/java')
executionData.from = files("$buildDir/outputs/code-coverage/connected/coverage.ec")
doFirst {
new File("$buildDir/intermediates/javac/fossReliantDebug/compileFossReliantDebugJavaWithJavac").eachFileRecurse { file ->
if (file.name.contains('$$')) {
file.renameTo(file.path.replace('$$', '$'))
}
}
}
}
task copyHebrewResources(type: Copy) {
File mergedResourcesDir = new File("${buildDir}/mergedResources")
if (!mergedResourcesDir.isDirectory())
mergedResourcesDir.mkdirs()
for (String resEach : android.sourceSets.main.res.srcDirs) {
File currentFolder = new File(resEach)
for (File folderContents : currentFolder.listFiles()) {
if (folderContents.name.endsWith("-he")) {
String iwNamedFolder = "${folderContents.name.substring(0, folderContents.name.lastIndexOf("-he"))}-iw"
from folderContents.path
into "${mergedResourcesDir.path}/${iwNamedFolder}"
println("Copying resource ${folderContents.path}")
}
}
}
println("Adding merged resource directories to the resources array ${mergedResourcesDir.path}")
android.sourceSets.main.res.srcDirs += mergedResourcesDir.path
}
preBuild.dependsOn copyHebrewResources
dependencies {
testImplementation 'junit:junit:4.12'
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation 'androidx.appcompat:appcompat:1.0.2'
annotationProcessor 'com.github.bumptech.glide:compiler:4.8.0'
googlePlayImplementation 'com.anjlab.android.iab.v3:library:1.0.44'
}
这是来自一个Android应用的部分build.gradle代码。
Plugins
Plugin在脚本中的意义类似常规编程中的import导包。除了Groovy和Gradle默认导入的基本API,其他的,比如android{...},都需要通过Plugin导入。可以通过PluginAware.apply(java.util.Map)或者PluginDependenciesSpec的方式导入插件包。这里的Map中的键不是随意设置的,支持from、plugin、to,每次apply类似传进去一句import指令,将插件中的API委托给当前的脚本对象。支持导入的插件类型应该是和Gradle版本绑定的,比如Android的Gradle插件,与Gradle版本之间就存在着兼容关系。
//PluginAware.apply(java.util.Map)
apply plugin: 'com.android.application'
apply plugin: 'jacoco'
//等价于
//PluginDependenciesSpec
plugins{
id 'com.android.application'
id 'jacoco'
}
Properties && Methods
在脚本中,最复杂的就是各种properties和methods。Gradle中的属性根据来源和功能被划分到5个集合中,按照Project对象的检索顺序依次为:
-
Project Properties,脚本默认自带属性一系列的项目属性,比如name等。可以直接通过属性名访问,也可以通过
project.属性名
的方式访问,本质上没区别。这些属性的读写依赖于对应的set/get方法,类似Spring等框架中常见的自动注入; - Extra Properties,这是项目于级维护的属性,便于各模块间交互,可以在project.ext这个namespace中进行自定义属性的键值对的声明、读取和更新;
- Extensions Properties,插件导入的只读属性,这些属性与extension同名;
- Convention Properties,这些属性是插件通过Project对象下的Convention对象传递给脚本的,其读写操作依赖于Convention对象;
-
Tasks Properties,我们可以通过
task名.属性名
的方式访问自定义或预定义的task的属性,当然,这种访问是只读的。
note:在本级project中检索不到时,会递归向上,一直到root project,检索extra properties或convention properties,只读。
Gradle中的Methods集也分为5个,而且,范围和检索顺序基本和Properties类似:
- Project对象自带Methods;
- build脚本中自定义的Methods;
- Convention Methods;
- Tasks,Gradle会为每个Task建立一个同名Method,其参数一般是Closure或Action。
note:
- 在本级project中检索不到时,会递归向上,一直到root project,检索需要的method;
- 以闭包为值得属性,在Method检索时,会被作为Method看待。
jacoco {
toolVersion = "0.8.5"
//<==>setToolVersion("0.8.5") 最终调用
//<==>setToolVersion "0.8.5" 快捷写法
//<==>toolVersion "0.8.5" 不规则但可编译
}
//<==>jacoco.toolVersion="0.8.5"
//<==>jacoco.setToolVersion("0.8.5")
jacoco是通过jacoco插件添加的属性,上面的例子中,我们对其中的toolVersion属性进行了配置。由于Delegate,我们可以直接访问属性对象内部的成员属性或方法,进行封装后,我们既可以通过闭包
进行批量的配置,也可以通过对象名.属性名
,进行单独配置。直接通过属性名访问的方式,使脚本更加易读和美观。虽然,无论怎么写,都可以达到目的。
def localPropertiesFile = project.rootProject.file('signing.properties')
//<==>def localPropertiesFile = rootProject.file('signing.properties')
project
是内置的属性,在不冲突混淆的情况下,可以省略。
compileSdkVersion 28
//<==>compileSdkVersion(28)
通常,对属性或方法的检索顺序是,属性集->同名Method->同名get/set方法->上一级。BaseExtension中有setCompileSdkVersion和getCompileSdkVersion方法,但是没有compileSdkVersion这个属性,所以会检索到compileSdkVersion(String version)这个方法调用。虽然通过属性名=属性值
或者属性名 属性值
的格式经过语法兼容后,都能进行有效的配置,但还是分类配置比较好,有同名属性或者set方法的,通过=赋值,有同名函数的通过快捷函数调用赋值,这样经过IDE的代码格式化后看起来会比较清晰。
Tasks
task jacocoTestReport(type: JacocoReport){...}
//<==>task([type:JacocoReport],"jacocoTestReport",{...})
//<==> tasks.create("jacocoTestReport",JacocoReport.class,{...})
前面的配置还是比较容易理解的,这行代码的原型就比较难懂了。这是Gradle提供的快捷写法之一,在理解这行代码原始形态的过程中,我也经历了比较多的猜测和推翻:
- Task构造函数,通过命名参数的特性以及快捷写法最终导致这样,但是这是通过task函数创建Task对象,并不是Task的构造函数,所以这种猜测被否定;
- Task名二次调用,先通过Task task(String name)创建Task对象,然后通过命令链、参数封装等特性进行二次配置,后来发现不存在二次调用的语法,这种猜测也被否定;
- Task task(Map, String, Closure),Groovy有将键值参数封装成Map作为第一个函数参数的特性,Gradle对Task的特殊封装,jacocoTestReport既是Task的变量名,也是Task名,允许不加引号定义,Groovy的快捷写法中,允许参数表末的闭包位于括号外,所以,最终觉得还是这种猜测比较靠谱。
配置闭包中的操作,对属性和方法的访问也遵循和Project级别类似的顺序,由内向外,由下向上。
classDirectories.from = fileTree(...)
//classDirectories = fileTree(
classDirectories是JacocoReport中的属性,具体的赋值因Gradle插件版本而异,老版本是直接赋值,新版本通过from赋值。
fileTree(
dir:...
excludes:...
)
这是文件集合的快捷函数,参数通过Map的形式导入,创建ConfigurableFileTree对文件集进行规则配置,常用的配置参数有dir、excludes、includes、builtBy等。
**/R$*.class
在配置excludes等参数时,通常会用到Ant Path路径匹配规则,乍一看和正则表达式还是挺像的。
? matches one character
* matches zero or more characters
** matches zero or more directories in a path
比如例子中展示的就是匹配所有R类中的内嵌类。
doFirst{...}
doFirst是Task中的流程函数,通过这一类的函数,用户可以进行task的前置后置等操作。
preBuild.dependsOn copyHebrewResources
preBuild是Task名,通过dependsOn等函数,可以改变Task的执行顺序。
Dependencies
dependencies {
testImplementation 'junit:junit:4.12'
implementation 'androidx.cardview:cardview:1.0.0'
googlePlayImplementation 'com.anjlab.android.iab.v3:library:1.0.44'
annotationProcessor 'androidx.annotation:annotation:1.0.2'
}
依赖就是将应用在编译过程需要的各种第三方包添加到项目库的操作,除了task,也就这个部分最容易出现问题,主要就是要求的依赖下载不下来的问题,可以参考另一篇文章查看对应的解决方案。API上的差异,Gradle 3.x以前的compile被废弃,分离为api和implementation,将依赖程度细化了。
- api,等同于compile,当前项目依赖的包,可以被依赖当前项目的项目访问,即,可导出式依赖;
- implemention,内部依赖,外部项目无法访问,所以优先使用implemention进行依赖。
库包名称格式,与Maven的大同小异,groudId:artifactId:version
,Maven仓库不仅提供Maven格式的依赖导入,也提供Gradle格式的导入。
Dependencies部分可繁可简,对于不同类型的库包选用的API也不一样, 比如,注解包就是通过annotationProcessor进行依赖。特殊情况在这些库包的引导页会有依赖导入样例,或者可以参考官方文档学习。
参考文章
Gradle Build Language Reference:https://docs.gradle.org/current/dsl/