Gradle 详解

gradle 是什么

       gradle 跟 ant/maven 一样,是一种依赖管理/自动化构建工具。但是跟 ant/maven 不一样,它并没有使用 xml 语言,而是采用了 Groovy 语言,创造了一种 DSL(特定领域语言),这使得它更加简洁、灵活,更加强大的是,gradle 完全兼容 maven 和 ivy。更多详细介绍可以看它的官网

       gradle 按照 gradle 的规则(build.gradle、settings.gradle、gradle-wrapper、gradle 语法)来进行构建。

AndroidStudio 中与 Gradle 相关的主要文件

       打开 AndroidStudio,新建一个工程,就可以看到如下目录结构:

AndroidStudio 中跟 Gradle 配置相关的文件

       Android Studio 中的 android 项目通常至少包含两个 build.gradle 文件,一个是 project 范围的,另一个是 module 范围的,由于一个 project 可以有多个 module,所以每个 module 下都会对应一个 build.gradle。project 下的 build.gradle 是基于整个 project 的配置,而 module 下的 build.gradle 是每个模块自己的配置。

       grade-wrapper.properties 文件主要是告诉开发工具,如果在电脑中找不到 Gradle 工具,那么要到哪个网址去下载 Gradle 工具,下载哪个版本的。

       整个工程目录下的 build.gradle 文件跟模块下的 build.gradle 文件名字相同,但是负责整个工程所有模块的构建。

       settings.gradle文件最简单,就是告诉 gradle 工具工程中包含哪几个模块。

settings.gradle

       打开这个文件,会看到如下内容:

1
include ':app'

       意思是我们这个工程中目前只有一个模块,模块的名字叫做 app,书写格式是 include 关键字后边跟一对单引号,单引号中是模块名称,名称前边还加了一个冒号,比如项目有两个模块 module-a、module-b,那么就需要在这个文件中进行配置,中间用英文逗号隔开即可,格式如下:

1
include ':module-a',':module-b'

grade-wrapper.properties

       打开这个文件可以看到下图所示内容:

1
2
3
4
5
6
#Sun Jan 26 18:51:01 CST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip

       这个文件告诉系统如果电脑上没有 Gradle 工具,要到哪个网址下载,所以整个文件最重要的就是最后一行,其中h的反斜杠是一个转义符,上面的那些 distributionPath 之类的配置是告诉系统如果需要下载 Gradle 工具的,下载完要保存在哪里。

project#build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
//构建过程依赖的仓库
repositories {
google()
jcenter()
}
//构建过程需要依赖的库
dependencies {
//下面声明的是 gradle 插件(plugin)的版本
classpath 'com.android.tools.build:gradle:3.5.0-beta04'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
//这里面配置整个项目依赖的仓库,这样每个 module 就不用配置仓库了
allprojects {
repositories {
google()
jcenter()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

       以 buildscript 为例,它是一个方法,可以加括号变成如下形式:

1
2
3
4
5
6
7
8
buildscript ({
repositories ({
google()
jcenter()
})
...
})
...

       此处的大括号是一个闭包,闭包相当于可以被传递的代码块或者方法。

       repositories 配置下面 dependencies 依赖的仓库地址,该依赖都是给 gradle plugin 使用的,比如 android 项目会用到下面的 plugin:

1
apply plugin: 'com.android.application'

       上面的 plugin 有可能是从网路上获取的,需要配置,配置的地方就是 dependencies 内的 classpath ‘com.android.tools.build:gradle:XXX’,它指定了需要使用哪些 plugin,这些 plugin 要从哪些地方哪些仓库下载,则在 repositories 内配置。

       仓库 repositories 声明了两次,这其实是由于它们作用不同,buildscript 中的仓库是 gradle 脚本自身需要的资源,表明 Gradle 工具本身要怎么配置,Gradle 工具本身要从哪个 maven 仓库下载,可以看到有一个是叫做 jcenter 的 maven 仓库,所谓 maven 仓库其实就是个网站,点击进入源码,可以看到 jcenter 的网址是 http://jcenter.bintray.com ,用浏览器打开可以看到里边按目录存放了很多 jar 包和其它的库文件,google 同理。

       allprojects 下的仓库是项目所有模块需要的资源。是我们工程里边所有模块的通用配置,这里规定所有模块中要用到的 jar 包也都从 google 和 jcenter 仓库中获取 ,在 allprojects 标签下配置的好处是你不需要再在每个模块下的 build.gradle 中单独配置了,如果你想在每个模块下单独指定用哪个仓库,那么这个全局的 build.gradle 中也可以不写。allprojects 事实上调用了一个方法,下面两种写法是等价的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
allprojects {
repositories {
google()
jcenter()
}
}

allprojects(new Action<Project>() {
@Override
void execute(Project project) {
repositories {
google()
jcenter()
}
}
})

module#build.gradle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//声明插件,这是一个android程序,如果是 android 库,应该是 com.android.library
apply plugin: 'com.android.application'
android {
//android 构建过程需要配置的参数
compileSdkVersion 21//编译版本
buildToolsVersion "21.1.2"//buildtool 版本

defaultConfig {//默认配置,会同时应用到 debug 和 release 版本上
applicationId "com.example.gradletest"//包名
minSdkVersion 15
targetSdkVersion 21
versionCode 1
versionName "1.0"
}

buildTypes {
//这里面可以配置 debug 和 release 版本的一些参数,比如混淆、签名配置等
release {
//release 版本
minifyEnabled false//是否开启混淆
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'//混淆文件位置
}
}
}

dependencies {
//模块依赖
compile fileTree(dir: 'libs', include: ['*.jar'])//依赖 libs 目录下所有 jar 包
compile 'com.android.support:appcompat-v7:21.0.3'//依赖 appcompat 库
}

apply plugin

       第一行的 apply plugin 是 Gradle 工具规定的写法,意思是我们要使用什么插件来构建项目,后边跟的是插件名称,com.android.application 是 Google 通过 Gradle 的 api,使用 Groovy 语言编写的一个插件,用于构建 Android 主工程。

       如果这个模块是一个 Library 的话,应该引入的插件叫做 com.android.library,如果熟悉 Gradle 的 api 的话也可以写自己的插件,github 上有许多辅助 Android 开发的插件,可以引入 AndroidStudio 中。

版本以及包名等常规配置

       android 标签下就是主要关注的地方,compileSdkVersion 是我们要用 Android 哪个版本的 api。

       buildToolsVersion 是构建工具的版本号,与我们在代码中调用的 api 没什么关系,是打包用的,建议用比较新的版本。

       applicationId 是我们应用的唯一标识:包名。如果这个模块是 Library,那么要把这行删掉。

       defaultConfig 中是一些基本配置,它会同时应用到 debug/release 版本上。

buildTypes 标签和 signingConfigs 标签

       buildTypes 表明我们可以打哪些类型的 apk 包,新建工程显示的只有 release 类型(debug 类型默认的,没有显示),意思是正式发布的 apk 要怎么打包。

       buildTypes 下的 debug 标签没有显示,但是在 Build Variants 下可以看到,在 release 标签和 debug 标签下,我们可以指定要用到的混淆文件在哪个目录下,签名文件(keystore)在哪个目录下,签名文件的密码之类的,如果没有配置的话就使用默认值。

       如果需要修改,则要在 android 标签下调用一个 signingConfigs 标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
android {
signingConfigs {
debug {
storeFile file('debug.keystore')
}
myConfig {
storeFile file('other.keystore')
storePassword '123456'
keyAlias = 'test'
keyPassword '123456'
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.myConfig
}
}
...
}

       signingConfigs 标签其实也是 com.android.application 插件中的,在下边随意增加配置,myConfig 这个名字是随意起的,默认的有两个:release 和 debug,如果想修改默认的打包配置就到这两个标签下改,如果想自己写一个就像上面这样写一个 myConfig 就可以了,里边写上用哪个 keystore 文件,keystore 的密码,然后不要忘了在 buildTypes 标签引用就可以了。

       另外,如果在开发过程中需要使用发布模式运行,可以通过 Project Structure 设置,也可以在 signingConfigs 标签中配置,如下所示:

1
2
3
4
5
6
7
8
9
10
11
android {
signingConfigs {
release {
storeFile file('D:\\AndroidStudioProjects\\MyProject\\AndroidProject\\GradleTest\\test.jks')
storePassword '123456'
keyAlias = 'test'
keyPassword '123456'
}
}
...
}

       项目中的 main 表示任何的 buildTypes 和 productFlavors 都会使用到,新增 debug 和 release 目录,以 GradleTest 为例,当版本为 debug 版本时,标题栏最左端会有绿色方块显示,而 release 版本则没有。所以 BuildTypesUtils 工具类代码要分成两份,其中 debug 版本下面的 BuildTypesUtils 是有绘制内容的,而 release 版本下面的 BuildTypesUtils是空实现的,需要注意的是,即便是空实现,但是 drawBadge 的方法还是要存在的,否则当运行 release 版本时,会提示方法找不到。

       有时候还会需要内测版,提供一些测试后门和命令,可以做如下配置:

1
2
3
4
5
6
7
8
buildTypes {
internal{
initWith debug
}
release {
...
}
}

       同时增加 internal 目录,加入 BuildTypesUtils,在内测版本中,标题栏最左端会有黄色方块显示。

       像 internal、release 是找不到方法的,但是 Gradle 会使用该方法的名称作为一个参数去调用别的方法。initWith debug 表示沿用 debug 的基本配置,当然可以在里面做一些自己的配置。

productFlavors 标签

       productFlavors 标签可以用来做多渠道打包,以及免费版和收费版,中国版和国际版等等。配置如下:

1
2
3
4
5
6
7
8
9
flavorDimensions 'price'
productFlavors {
free {

}
paid {

}
}

       此时 Build Variants 里面的选项变成了 productFlavors 和 buildTypes 的组合,像 freeDebug、paidInternal 等等。

       productFlavors 标签也可以配置多个,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
flavorDimensions 'price', 'nation'
productFlavors {
free {
dimension 'price'
}
paid {
dimension 'price'
}
china {
dimension 'nation'
}
global {
dimension 'nation'
}
}

       Build Variants 里面的选项就更多了。

dependencies 标签

整体概述

       dependencies 这个标签下就是依赖的 jar 包、第三方库或者 library 工程,如下所示:

1
2
3
4
5
6
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.13'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'

       这几行是新建工程默认生成的,其中第一行表明工程中要用到的 jar 包都可以放在 app 下的 libs 目录中,有了这一行配置,我们把 jar 包放在 libs 目录下即可,不需要再做额外的配置了。

       下面以 v7 支持包进行说明:

1
implementation 'com.android.support:appcompat-v7:23.4.0'

       表明到 maven 库中找 v7 支持包,单引号中内容被冒号分成了三个部分,com.android.support 是这个支持包在 maven 库中的存放路径,appcompat-v7 是这个包的名称,23.4.0是这个包的版本,有了这个配置,Gradle 工具就会到我们在全局 gradle 文件中指定的 maven 库中下载相应的支持包了,其中版本号可以不写23.4.0,写成23.4.+,意思就是如果支持包出了新版本,比如出了23.4.1,那么就用23.4这个分支下的最新版本,如果写成23.+,就是始终使用23这个版本下最新的分支版本,如果干脆写成一个+号,比如像下面这样

1
implementation 'com.android.support:appcompat-v7:+'

       就是告诉编译器,始终使用最新版的 v7 支持包,比我们手动拷贝 jar 包或 aar 包要灵活多了,不过还是不建议这样写,因为 gradle 会始终访问网络去查询有没有新的包,而且也无法确定最新的包是不是一定适合,有没有兼容旧版的代码。

       然后 dependencies 标签下还可以配置引用了哪些 Library 模块,如果 app 主模块有引用 Library 工程,就要在 dependencies 标签下指定 Library,像这样:

1
implementation project(':my-library-module')

       project 后面括号中的内容要与 settings.gradle 文件中写的模块名称一致,也是以冒号开头,并且用单引号括起来。

compile、implementation 和 api

  • implementation 不会传递依赖;
  • compile / api 会传递依赖,在新版本中,compile 已被弃用。api 是 compile 的替代品,效果完全等同;
  • 当依赖被传递时,二级依赖的改动会导致零级项目重新编译;当依赖不传递时,二级依赖的改动则不会导致零级项目重新编译,减少编译时间消耗。

项目中的其余配置文件

gradle.properties

       从名字上就知道它是一个配置文件,这里可以定义一些常量供 build.gradle 使用,比如可以配置签名相关信息如 keystore 位置、密码、keyalias 等。

gradlew 和 gradlew.bat

       这分别是 linux 下的 shell 脚本和 windows 下的批处理文件,它们的作用是根据 gradle-wrapper.properties 文件中的 distributionUrl 下载对应的 gradle 版本(如果当前电脑上没有的话)。这样就可以保证在不同的环境下构建时都是使用的统一版本的 gradle,即使该环境没有安装 gradle 也可以,因为 gradle wrapper 会自动下载对应的 gradle 版本。gradlew 的用法跟 gradle 一模一样,比如执行构建 gradle build 命令,你可以用 gradlew build。gradlew 即 gradle wrapper 的缩写。

Gradle Task

       使用方法:gradlew taskName。

       task 的结构如下:

1
2
3
4
5
6
7
8
9
task taskName {
初始化代码
doFirst {
task 代码
}
doLast {
task 代码
}
}

       下面是一个实际的例子,在 app 目录下增加 version.properties 文件,记录版本名称和版本号:

1
2
3
#Mon Jan 27 20:06:40 CST 2020
VERSION_NAME=1.0.0
VERSION_CODE=2333

       并在 app 的 build.gradle 内增加如下代码:

1
2
3
4
5
6
7
8
9
10
task bumpVersion(){
doLast{
def versionPropsFile = file('version.properties')
def versionProps = new Properties()
versionProps.load(new FileInputStream(versionPropsFile))
def codeBumped = versionProps['VERSION_CODE'].toInteger() + 1
versionProps['VERSION_CODE'] = codeBumped.toString()
versionProps.store(versionPropsFile.newWriter(), null)
}
}

       在 Android Studio 的 Terminal 内输入 gradlew bumpVersion,执行上面的 task,version.properties 中的 VERSION_CODE 加一。

       如果将 doLast 和大括号去掉,那么不论在 Terminal 内输入 gradlew、gradlew bumpVersion 还是 gradlew clean 等等,上面的代码都会执行。task 任务代码要写在 doLast 或者 doFirst 内。事实上下面的两段代码是等效的:

1
2
3
4
5
6
7
8
9
10
11
task bumpVersion(){
def versionPropsFile = file('version.properties')
//此处省略增加版本号的代码
...
}



def versionPropsFile = file('version.properties')
//此处省略增加版本号的代码
...

       doFirst()、doLast() 和普通代码段的区别:

       普通代码段在 task 创建过程中就会被执行,发生在 configuration 阶段;doFirst() 和 doLast() 在 task 执行过程中被执行,发生在 execution 阶段,在普通代码段后面执行。如果用户没有直接或间接执行 task,那么它的 doLast()、doFirst() 代码不会被执行,例如在增加版本号的任务中加入如下打印:

1
2
3
4
5
6
7
8
9
task bumpVersion(){
println "A"
doLast{
def versionPropsFile = file('version.properties')
//此处省略增加版本号的代码
...
}
println "B"
}

       在 Terminal 内输入 gradlew,那么只有 println “A” 和 println “B” 会执行,doLast 中增加版本号的方法是不会执行的,gradlew 并没有执行 task;如果在 Terminal 内输入 gradlew bumpVersion,增加了 taskName,则打印和增加版本号的方法都会执行。

       doFirst() 和 doLast() 都是 task 代码,其中 doFirst() 是往队列的前面插入代码,doLast() 是往队列的后面插入代码。

       task 的依赖:可以使用 task taskA(dependsOn: b) 的形式来指定依赖。指定依赖后,task 会在自己执行前先执行自己依赖的 task。在上面的例子中,我们模拟增加版本号之后代码提交 git 的方法,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
task bumpVersion(){
doLast{
def versionPropsFile = file('version.properties')
//此处省略增加版本号的代码
...
}
}

task commitVersion(dependsOn: bumpVersion) {
doLast {
println "提交代码!"
}
}

       由于 commitVersion 依赖 bumpVersion,这样在 Terminal 内输入 gradlew commitVersion,则会先执行 bumpVersion 再执行 commitVersion,即“增加版本号之后代码提交 git”。

gradle 执行的生命周期

       gradle 执行有三个阶段:

  • 初始化阶段:执行 settings.gradle,确定主 project 和子 project
  • 定义阶段:执行每个 project 的 bulid.gradle,先执行 project#build.gradle 再执行 module#build.gradle,确定出所有 task 所组成的有向无环图;
  • 执行阶段:按照上一阶段所确定出的有向无环图来执行指定的 task。

       如果需要在各个阶段插入代码,在一二阶段之间,可以在 settings.gradle 的最后 插入,如下所示:

1
2
include ':app', ':library1', ':library2'
//在初始化阶段和定义阶段之间插入的代码

       在二三阶段之间插入代码,需要写在 project#build.gradle 的 afterEvaluate 中,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
buildscript {
repositories {
...
}
dependencies {
...
}
}

allprojects {
repositories {
...
}
}
afterEvaluate {
//在定义阶段和执行阶段之间插入的代码
}

       比如需要动态添加 Task 的情况,在有向无环图没有确定之前 Task 是不存在的,则需要在二三阶段之间插入代码。

参考资料:
gradle入门
小浩_w android signingConfigs打包配置
applixy 从Eclipse到AndroidStudio(四)Gradle基本配置
腾讯课堂 HenCoder

Fork me on GitHub