永恒的码流

万物皆流,无物常驻

0%

Android构建03-Gradle基础

前言

Gradle是Android构建系统的重点,需要花费时间用心学习。学习资料主要是官方的Gradle Docs,有个社区版的中文翻译资料,英语不好的同学可以看看,但不全。

这章首先对Gradle做一个简单的介绍,然后讲如何使用官方文档和API,接着讲解Gradle的基本概念、Wrapper和学习中需要使用到的命令行指令、插件等。有些知识点如插件应该放在之后的章节讲解,但为了保持文章的完整性,我放在了这一章,学习的过程中可能不能完全理解,没有关系,在以后的章节中如果有涉及的话,会具体解释。

Gradle简介

Gradle是一个注重灵活性和性能的开源构建自动化工具,使用Groovy或Kotlin DSL来编写构建脚本。

虽然支持Kotlin,但还是建议学习Groovy。因为:一,目前官方示例里有许多还没有kotlin方式;二,目前大多数项目的Gradle脚本都是用Groovy编写的。

Gradle特点如下:

  1. 高度可定制。Gradle使用了可定制和可扩展的设计思想,如使用插件。
  2. 快速。Gradle通过重用先前执行的输出,仅处理已更改的输入以及并行执行任务来快速完成任务。
  3. 强大。Gradle是Android的官方构建工具,并支持许多主流的编程语言和技术。

图形界面和命令行 Gradle支持IDE或命令行两种方式来构建工程。支持主流的IDE,如Android Studio, Eclipse, IntelliJ IDEA, Visual Studio 2017, 和XCode等。

提示:命令行方式构建是必须掌握的,因为命令行方式包含所有的功能,而图形界面方式却未必。在调试时,命令行方式尤其重要。

典型的工程目录 一个典型Gradle项目一般会包括两个脚本文件(settings.gradlebuild.gradle)和Wrapper文件(gradle文件目录和gradlew、gradlew.bat可执行文件)。如下所示

1
2
3
4
5
6
7
8
├── settings.gradle 
├── build.gradle
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
└── gradlew.bat

其中settings.gradlebuild.gradle是项目构建时所需的脚本,Gradle会据此创建相应的Java实例。Wrapper文件用于管理gradle版本,一个终端上不同的项目可以使用不同配置和版本的Gradle,具体项目中使用./gradlewgradlew.bat来替代gradle命令。



Gradle Docs的使用

Gradle Docs资料中,最重要的是User Manual(用户手册)和API文档(DSL Reference和Javadoc),其余的可以不用看。

Gradle User Manual User Manual中最关键的一部分当属“Build Configuration Scripts”(官网经常换名字,可能已经不叫这个标题了)。这一部分内容相当多,内容也比较杂。可以先看Build LifecycleConfiguring Multi-Project Build这两节,看懂这两节,就可以对Gradle构建的流程有一个清晰的了解了,之后再看其他章节就非常容易了。

API文档 API文档包括Gradle DSL Reference(Gradle特定语言指南)Javadoc(Java文档)。Reference包含主要的接口和类,并做了描述和分类以利于学习,Javadoc包含所有的API,主要用于检索。就内容的覆盖面而言,Javadoc完全包含Reference。学习时可以看Reference,使用中具体查找Gradle的某个API文档时,用Javadoc。



基础概念

使用Gradle构建项目时,会经常涉及到几个重要的概念:脚本(Script)、项目(Project)和任务(Task)。这几个概念出现的顺序如下:

  1. 开发者编写脚本,如settings.gradlebuild.gradle
  2. 构建项目时,Gradle会根据构建脚本创建对应的Java实例,如settings.gradle对应Settingbuild.gradle对应Project,其中Project包含构建需要的任务集。然后,Gradle根据这些实例依次执行初始化、项目配置和任务执行等来构建项目。

脚本(Script)

利用Gradle构建项目时,与开发者直接交互的就是脚本了。Gradle脚本有以下特点

  1. 每个脚本都有一个对应的Java实例,称为代理对象,在脚本中可以直接使用代理对象的属性和方法。
  2. Gradle脚本实现了Script接口。此接口定义了许多属性和方法,可以在脚本中直接使用。
  3. Gradle脚本由零或多个语句和脚本块组成(statements and script blocks)。 语句包括方法调用、属性赋值和局部变量定义等。 脚本块是一种方法调用,它以闭包作为方法的参数,而闭包用来配置代理对象。

项目(Project)

项目是Gradle的最基本的概念之一。
Gradle通过build.gradle脚本生成实现了Project接口的项目实例,每个Gradle构建都由一个或多个项目组成。Project接口是脚本文件与Gradle交互的主要API,可以通过Project实例访问Gradle的所有功能。

任务(Task)

项目由一个或多个任务组成。 每个任务执行一些基本工作,例如编译类、运行单元测试或压缩WAR文件等。
任务由一系列Action对象组成。 执行任务时,通过调用Action.execute(T)依次执行每个Action。 可以通过调用Task.doFirst(org.gradle.api.Action)Task.doLast(org.gradle.api.Action)向任务添加Action

概念之间的关系 Gradle中项目和任务、Action的关系如下:

1
2
3
4
5
6
7
8
9
10
11
graph TD
A[Gradle构建]
A -->B1(项目1)
A -->B2(项目2)
A -->B3(项目 ...)
B1 --> T11[任务 ...]
T11 --> A111[Action ...]
B2 --> T21[任务 ...]
T21 --> A211[Action ...]
B3 --> T31[任务 ...]
T31 --> A311[Action ...]

浏览官方文档 以上概念详细说明和使用,请见官方手册和指南:

  1. 脚本。Writing Build ScriptsScript Reference
  2. 项目。Project Reference
  3. 任务。Build Script BasicsAuthoring TasksWriting Custom Task ClassesTask Reference

以上文档可以不用深读,但需要浏览一遍,知道大概。



Gradle Wrapper

前言中已经提到了Wrapper,即一种管理当前项目的Gradle版本的工具。多人协作时,每个人安装的Gradle环境可能不一致(也没有必要一致),需要使用Wrapper管理项目的版本。官方强烈推荐使用Wrapper管理具体的项目,并以./gradlew(macOS)代替gradle执行命令。
Wrapper目录中有一个gradle-wrapper.properties文件,配置Gradle的版本号、本地存储地址等。

引入Wrapper 使用命令行添加Wrapper有两种方式:

  1. 使用gradle init创建新项目,则会初始化一个带有Wrapper的Gradle项目。
  2. 使用gradle wrapper在旧的项目中添加Wrapper。

    wrapper是Gradle的内建任务

一般的IDE创建项目时都会自动生产Wrapper文件,如Android Studio。

使用Wrapper执行任务 用Wrapper脚本替换掉gradle来执行任务即可。以macOS平台为例,在项目根目录下执行./gradlew [task name] 即可,如列出当前项目的所有任务,项目根目录下执行:

1
$ ./gradlew tasks

更新Wrapper 有两种方式更新Wrapper

  1. 命令行方法。.gradlew wrapper --gradle-version [要更新的版本号]
  2. 修改gradle/wrapper/gradle-wrapper.properties中的distributionUrl属性。

关于Gradle Wrapper的详细说明请见官方手册The Gradle Wrapper



命令行

指令形式 Gradle命令行指令由三部分组成: 可执行文件gradle./gradlew、一个或多个任务名、零或多个选项名,如下所示:。

1
$ gradle [taskName...] [--option-name...]

指令的详细说明请见官方手册Command-Line Interface,这里我只介绍学习或调试中常用的指令。部分常见命令行选项说明如下:

固有任务类

  • gradle build。生成所有的输出,并执行所有的检查。
  • gradle clean。删除build文件目录。
  • gradle projects。查看项目结构。
  • gradle tasks。查看任务列表。查看某个任务详细信息,可用gradle help --task someTask
  • gradle dependencies。查看依赖列表。

调试类

  • -?, -h, --help。查看帮助信息。
  • -v, --version。查看版本信息。
  • -s, --stacktrace。执行任务时,打印栈信息。如gradle build -s

日志类

  • -q, --quiet。只打印errors类信息。
  • -i, --info。打印详细的信息。

其中在学习的过程中,经常会用到

1
$ gradle [taskName...] -i

这会打印比较详细的构建信息,用于帮助我们理解Gradle构建流程、项目配置和依赖、任务依赖等。



环境配置

配置构建环境,主要配置Gradle构建参数和对应的JVM参数,如代理策略等。其目的是为了多人协作时,保持在一致的环境下进行项目开发。

配置环境有几种途径,优先级从高往低,列出如下:

  1. 命令行。
  2. GRADLE_USER_HOME目录中的gradle.properties文件。
  3. 项目根目录中的gradle.properties文件。
  4. 环境变量。运行Gradle环境的变量,如JAVA_HOME等。

具体参数的配置请见官方手册Build Environment



依赖管理

现在的项目基本上都需要使用第三方库,一般称其为依赖,这样说并不准确,官方的定义是:

A dependency is a pointer to another piece of software required to build, test or run a module

依赖是指向软件模块的指针,而非模块本身

依赖管理是一种以自动方式声明、解析和使用项目所需的依赖的技术。

依赖管理流程

  1. 声明。在构建脚本中声明依赖和对应的仓库。
  2. 解析。项目构建时,Gradle会定位依赖的本地路径或远程服务器路径并下载依赖(有必须要的话)。
  3. 缓存。依赖解析后,Gradle会将从远程服务下载的依赖缓存到本地,避免下次构建时重复下载。

依赖的类型

  • 模块依赖(Module dependencies)。指向一个库中的模块,这是最常见的依赖类型。
  • 文件依赖。指向无需仓库的一系列文件。
  • 项目依赖。指向多项目工程主项目需要用到的其他项目。
  • 特定分发的依赖。如API依赖、TestKit依赖和本地Groovy依赖等。

常见的仓库

  • 文件系统仓库。通过查找文件目录以解析依赖。
  • Maven Central 仓库。一般默认的Maven仓库。
  • JCenter Maven 仓库。Android默认的Maven仓库。
  • Google Maven 仓库。Google的支持库所在的Maven仓库。
  • 本地 Maven仓库。就是你开发项目所用的主机中的仓库。
  • 自定义Maven仓库。一些公司,如阿里,喜欢把阿里云OSS服务SDK等存储在自己搭建的Maven仓库中。
  • Ivy仓库。不常用。

Maven仓库是用Apache Maven工具搭建的存储不同类型资源的本地或远程资源服务。Maven Central仓库、JCenter Maven仓库和Google Maven仓库都是Maven仓库,区别在于存储的服务器和管理的机构不一样。



插件(Plugin)

插件简介 Gradle的一个非常大的特点就是灵活或者说可扩展性,它本身没有提供多少构建具体任务的逻辑,构建各类具体的项目都是通过增加插件实现的。插件可以添加新任务(例如JavaCompile),新的域对象(例如SourceSet),新的规定(例如Java源位于src/main/java)以及继承其他插件而获得的功能。
Gradle本身提供了一些基础的插件,只需要配置id即可使用,如 Java Plugin

二进制插件及配置 插件分为两种,本地脚本插件和远程发布的二进制插件,一般我们常用的是后者。如Android项目中我们用到的Android插件,就是一种远程二进制插件。
Android项目添加插件示例:
在根目录build.gradle中添加仓库和插件路径

1
2
3
4
5
6
7
8
9
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.4'
}
}

在app目录build.gradle中应用插件

1
apply plugin: 'com.android.application'

插件的使用 Gradle使用插件里的构建逻辑分为两步

  1. 解析插件。即获取插件的jar包。插件被解析后,就可以在脚本文件中直接使用它的API,如Android项目脚本文件中的配置android、方法compileSdkVersion等。本地脚本插件和Gradle固有插件会自动解析。
  2. 应用插件。即执行插件的Plugin.apply(T)方法,这是插件的核心方法。在这个方法中,可以为当前项目添加一些新的任务。

关于插件更详细的说明请见官方手册:

  1. Using Gradle Plugins
  2. Standard Gradle plugins
  3. Core Plugin
  4. Writing Custom Plugins

如何你想查看Gradle源码,看看插件是如何实现的,链接如下:

  1. Github上的Gradle源码
  2. Android插件源码-AOSP
  3. Andorid插件源码-repe.spring.io
  4. Android源码-android.google.source


示例-单项目构建

官方示例 Creating New Gradle Builds
主要练习:初始化项目、执行任务、移动文件、添加Plugin等。

提示:示例中使用了./gradlew来执行命令,如果因为下载失败导致该命令不可用,使用gradle命令即可。



示例-多项目构建

官方示例 Creating Multi-project Builds

提示:示例中需要下载依赖,国内网络直接访问可能导致下载失败,需要设置国外代理服务。我的代理服务走的通道是shadowsocks,有的资源使用shadowsocks协议是访问不了的,需要使用httphttps协议才行,可以使用privoxy切换shadowsocks协议为httphttps协议(实际上是在本地shadowsocks端口上再加了一层privoxy代理)。
shadowsocks代理设置请参考Github shadowsocks
privoxy代理设置请参考 Privoxy 官网使用Privoxy桥接Http代理到SOCKS5代理Privoxy 教程
Gradle项目代理设置如下:在电脑中设置好http协议代理后,在当前项目根目录下创建gradle.properties文件(如果有就不用创建),编辑文件如下:

1
2
3
4
5
6
systemProp.http.proxyHost=127.0.0.1
systemProp.http.nonProxyHosts=192.168.*, <localhost>
systemProp.http.proxyPort=8118
systemProp.https.proxyHost=127.0.0.1
systemProp.https.nonProxyHosts=192.168.*, <localhost>
systemProp.https.proxyPort=8118

其中8118privoxy的端口号
Gradle代理设置详细说明请参考官方文档Accessing the web via a proxy(虽然也不够详细:p)



示例-Android项目根目录 build.gradle语法分析

利用Android Studio创建一个项目,根目录中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
buildscript {
ext.kotlin_version = '1.2.50'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.1.4'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}

allprojects {
repositories {
google()
jcenter()
}
}

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

直接使用API文档来分析上面的代码:

  • Project和方法。build.gradle脚本的代理对象是Project实例,buildscriptallprojectstaskProject中的三个方法(实际上,task在这里是关键字,不是方法,但Project中有方法与之对应)。

  • buildscript。在Android Studio中(Mac平台 Command+鼠标左键)或者在文档中查看API详情,如下
    buildscript
    这个方法的作用是配置项目构建脚本的classpath,参数是一个闭包,闭包的代理对象是ScriptHandler,即如果buildscript代码块中的属性和方法,如果不能在Project中找到,就去ScriptHandler中去找。
    再来分析下buildscript代码块里的内容。示例代码中,repositoriesdependencies都是ScriptHandler的方法,可以直接在Android Studio中点击查看其API。但ext是什么,是谁的属性?ownerProject)的,还是delegateScriptHandler)的?由于不能在Studio里直接查看,我们先看看Project的API
    Project
    全局搜索extsetExtgetExt,都没有结果。再看看其继承的接口,发现ExtensionAware中可以搜索到,API文档说

1
2
3
4
5
// All extension aware objects have a special “ext” extension of type >ExtraPropertiesExtension
assert project.hasProperty("myProperty") == false
project.ext.myProperty = "myValue"
// Properties added to the “ext” extension are promoted to the owning >object
assert project.myProperty == "myValue"

继承了ExtensionAware的对象都有一个特殊的ext扩展类型,可以直接添加属性project.ext.myProperty = "myValue",之后使用属性myProperty时可以不用写ext,如project.myProperty
因此示例代码中的extownerProject)的属性。

  • allprojects。分析方法同上,略。

  • taskProject中有四个task方法

Task task(String name)
Task task(String name, Closure configureClosure)
Task task(Map<String, ?> args, String name)
Task task(Map<String, ?> args, String name, Closure configureClosure)

Studio中链接的是Task task(String name),但后面的clean(type: Delete) { ​ delete rootProject.buildDir }是什么呢?开始以为是方法,实际上并不是,一是在Project中没有clean方法,二是Groovy语法中不允许嵌套的方法省略括号

Parentheses are required for method calls without parameters or ambiguous method calls:

1
println(Math.max(5, 10))

不是方法,但看起来又不是参数。查看TaskAPI,有定义task的示例
You can also use the task keyword in your build file:

1
2
3
4
task myTask
task myTask { configure closure }
task myTask(type: SomeType)
task myTask(type: SomeType) { configure closure }

也就是说task可以是一个关键字!去看Gradle官方文档Defining tasks
There are a few variations on this style, which you may need to use in certain situations. For example, the keyword style does not work in expressions.

有很多方式定义task,有关键字格式:

1
2
3
4
task copy(type: Copy) {
from(file('srcDir'))
into(buildDir)
}

我们的示例就是这种格式的。
也有方法格式的:

1
2
3
4
task('copy', type: Copy) {
from(file('srcDir'))
into(buildDir)
}

这对应Project的方法Task task(Map<String, ?> args, String name, Closure configureClosure)。Groovy方法中的参数似乎是可以改变顺序的,有人写的博客中提到了这件事,但我没有找到相关的官方文档。
终于分析完了,初学者学习这些估计要抓狂,:p。

最后,把示例代码中省略的括号和ownerdelegate补全,以提高可读性,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
buildscript({
owner.ext.kotlin_version = '1.2.50'
delegate.repositories({
delegate.google()
delegate.jcenter()
})
dependencies({
delegate.add('classpath', 'com.android.tools.build:gradle:3.1.4')
delegate.add('classpath', "org.jetbrains.kotlin:kotlin-gradle-plugin:${owner.kotlin_version}")
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
})
})

allprojects({
delegate.repositories({
delegate.google()
delegate.jcenter()
})
})

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

注意: 其中的classpath,我暂时并没有理解清楚它是如何工作的,它不是方法,只能理解它是一个可以添加的配置属性。

1
Dependency add(String configurationName, Object >dependencyNotation);


参考

  1. Gradle官网
  2. Gradle User Guide 中文版(非官方)
  3. Groovy官网