XDRush

一篇文章带你了解Android之Gradle

1 Groovy & Gradle基础

1.1 Groovy

这里不作介绍,像写脚本一样写Java,也是运行在JVM上。比较简单,是Gradle基础,当然,也可以用纯Java来写gradle。

1.2 Gradle

Gradle是一个工具,同时它也是一个编程框架。使用这个工具可以完成app的编译打包等工作,当然也可以用它干其他的事情。Gradle相关文档见这里,我们编写所谓的编译脚本,其实就是在用Gradle提供的API。

Gradle是一个框架,它提供了很多基本组件:Gradle中,每一个待编译的工程都叫一个Project。每一个Project在构建的时候都包含一系列的task。比如一个Android APK的编译可能包含:Java源码编译task、资源编译task、JNI编译task、lint检查task、打包生成APK的task、签名task等。每个Project具体包含多少个task是有编译脚本指定的插件决定的,这个在后面会讲到。

Gradle作为一个框架,它负责定义流程和规则。而具体的编译工作则是通过插件的方式来完成的。比如编译Java有Java插件,编译Groovy有Groovy插件,编译Android APP有Android APP插件,编译Android Library有Android Library插件等等。

1.2.1 Gradle对象

Gradle主要有三种对象,这三种对象和三种不同的脚本文件对应,在gradle执行的时候,会将脚本转换成对应的对象:
(1) Gradle对象:当我们执行gradle xxx或者什么的时候,gradle会从默认的配置脚本中构造出一个Gradle对象。在整个执行过程中,只有这么一个对象。Gradle对象的数据类型就是Gradle。我们一般很少去定制这个默认的配置脚本。
(2) Project对象:每一个build.gradle会转换成一个Project对象。
(3) Settings对象:显然,每一个settings.gradle都会转换成一个Settings对象。

其中最重要的就是project对象,project包含若干个tasks,在project中,我们要做如下工作:

  • 加载插件
  • 配置插件
  • 设置属性

加载插件

加载插件是调用它的apply函数,我们常见的就有以下几个:

1
2
3
apply plugin: 'com.android.library'
apply plugin: 'comn.android.application'
apply from: "$project.rootDir/build.gradle" // 加载另外一个**.gradle文件,通常定义一些公共的模块

配置插件

配置插件这里就不作多说,后文会详细讲到。

设置属性

如果是单个脚本,则不需要考虑属性的跨脚本传播,但是Gradle往往包含不止一个build.gradle文件,比如我设置的utils.gradle,settings.gradle。如何在多个脚本中设置属性呢?

Gradle提供了一种名为extra property的方法。extra property是额外属性的意思,在第一次定义该属性的时候需要通过ext前缀来标示它是一个额外的属性。定义好之后,后面的存取就不需要ext前缀了。ext属性支持Project和Gradle对象。即Project和Gradle对象都可以设置ext属性。后文会讲到。

2 Android Gradle简介

目前Android基本上都是采用Gradle进行构建,针对大家对gradle用法的一些疑惑,下面将详细剖析gradle在android构建中的用法。

2.1 Android Gradle目录结构

首先来看看一个典型的Android工程中gradle的结构:

一般Android项目Gradle结构

上图基本就是目前典型的Android结构(Dolphin里面的更为复杂,相对Gradle用法有进一步了解的,请参考Dolphin),万变不离其宗,理解了这些,再复杂的配置都能清晰看懂。

2.2 Top Level Gradle

Top Level Gradle用于配置所有Module的属性,包括一些公共的方法、公共的依赖项。一般包含3个文件,build.gradle、settings.gradle、gradle.properties。下面来依次了解下每个文件是用来干什么的。

2.2.1 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
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    // Top-level build file where you can add configuration options common to all sub-projects/modules.
    buildscript {
    repositories {
    jcenter()
    }
    dependencies {
    classpath 'com.android.tools.build:gradle:1.2.3'
    // NOTE: Do not place your application dependencies here; they belong
    // in the individual module build.gradle files
    }
    }
    allprojects {
    repositories {
    jcenter()
    }
    }
    task clean(type: Delete) {
    delete rootProject.buildDir
    }
    /**
    * 定义外部属性,所有Module中均用这里的配置,避免Project修改时所有Module都要更改.
    */
    ext {
    // Android config
    androidBuildToolsVersion = '23.0.2'
    androidMinSdkVersion = 16
    androidTargetSdkVersion = 23
    androidCompileSdkVersion = 23
    androidVersionCode = 1
    androidVersionName = '1.0'
    // libraries config
    supportVersion = '23.1.1'
    picasso = '2.5.2'
    commonDependencies = [
    appcompatV7 : "com.android.support:appcompat-v7:${supportVersion}",
    design : "com.android.support:design:${supportVersion}",
    picasso : "com.squareup.picasso:picasso:${picasso}"
    ]
    }

接下来来具体看看top level构建脚本下每个节点代表什么。

  • buildscript节点
    buildscript节点是运行该构建脚本所需要用到的依赖文件,repositories则指定了依赖文件的查找位置。一般而言,这个script block都写在脚本开头,声明这个脚本本身运行所需的依赖。

  • allprojects
    设置当前project以及所有sub project中依赖文件的查找路径。

  • task clean
    定义一个新的task叫做clean,类型为Delete。Android Plugin内置了clean方法,但是该方法在module中,module中内置的clean方法只会清理module中的文件并删除module中的build目录,但是工程根目录中的build文件没有被删除。因此,这里定义的clean方法删除根目录下的build文件夹。

2.2.2 settings.gradle文件

  • 示例

    1
    include ':app', ':third_party'
  • 说明
    管理所有sub project,凡是要被编译的子项目,都要写在这里,gradle按照这个配置递归编译子项目。

2.2.3 gradle.properties

配置gradle JVM运行的一些参数,MaxHeapSize之类的,本地环境配置。

2.3 Module Level Gradle

模块级的脚本描述的是该模块的编译过程,一般而言,Android用到的有两种:

  • Application:apply plugin: ‘com.android.application’,编译结果为apk

  • Library:apply plugin: ‘com.android.library’,编译结果为aar

application插件和library插件内容配置差不多,这里分别用讲述以2中形式来分别配置Module的gradle.

2.3.1 application build.gradle代码示例

首先来看第一种方式,直接读取top level的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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
apply plugin: 'com.android.application'
/**
* 定义外部属性.
*/
ext {
app_name = "Gradle Demo"
}
/**
* 配置Android属性,有很多属性可以配置,具体可以参考{@link com.android.build.gradle.BaseExtension}
*/
android {
/**
* 获取project配置。
*/
def globalConfiguration = rootProject.extensions.getByName("ext")
compileSdkVersion globalConfiguration.getAt("androidCompileSdkVersion")
buildToolsVersion globalConfiguration.getAt("androidBuildToolsVersion")
defaultConfig {
applicationId "io.xdrush.com.gradledemo"
minSdkVersion globalConfiguration.getAt("androidMinSdkVersion")
targetSdkVersion globalConfiguration.getAt("androidTargetSdkVersion")
versionCode globalConfiguration.getAt("androidVersionCode")
versionName globalConfiguration.getAt("androidVersionName")
// manifest占位符替换.
manifestPlaceholders = [app_name: app_name]
}
packagingOptions {
exclude 'main/AndroidManifest.xml'
}
signingConfigs {
debug {
}
/**
* 配置Release版签名文件.
*/
release {
keyAlias 'm8u'
keyPassword 'com.cyou.m8u'
storeFile file("key/m8u")
storePassword 'com.cyou.m8u'
}
}
/**
* type默认会有debug和release,通常在debug中保留默认值,release中开启混淆,并使用签名文件进行签名.
*/
buildTypes {
debug {
// 使用默认值.
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
}
dependencies {
/**
* 获取project commonDependencies.
*/
def dependencies = rootProject.ext.commonDependencies
compile dependencies.appcompatV7
compile dependencies.design
// compile dependencies.picasso
/**
* 导入libs文件夹下所有的jar文件.
*/
compile fileTree(dir: 'libs', include: ['*.jar'])
/**
* 本地aar依赖.
*/
compile(name: 'trendingsdk-1.1.1', ext: 'aar')
/**
* 配置依赖Module.
*/
compile project(':third_party')
}
/**
* 配置本地libs为依赖查找源,当需要导入本地.aar文件时需要这个.
*/
repositories {
flatDir {
dirs 'libs'
}
}

2.3.2 library build.gradle代码示例

第二种读取方式则更为简单,直接导入top level的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
apply plugin: 'com.android.library'
apply from: "$project.rootDir/build.gradle"
android {
compileSdkVersion androidCompileSdkVersion
buildToolsVersion androidBuildToolsVersion
defaultConfig {
minSdkVersion androidMinSdkVersion
targetSdkVersion androidTargetSdkVersion
versionCode androidVersionCode
versionName androidVersionName
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile commonDependencies.appcompatV7
compile fileTree(dir: 'libs', include: ['*.jar'])
}

推荐使用这种方式来读取ext值。

3 Gradle之task详解

以上还只是了解gradle的基本配置,而gradle构建最最核心的部分是各个插件提供的task,每个task独自执行自己的任务,将构建一步步拆分。

3.1 task简介

task是gradle中的一种数据结构,它代表一些要被执行的工作,是构建任务的最基本执行单元,不同的插件有不同的task,每一个task都需要和一个project相关联。

third_party Module的gradle目录

app Module的gradle目录

可以看到,上面属于library插件的third_party的task和属于application插件的app的task是不相同的。
需要强调的一点是,org.gradle.api.Project这个类是build.gradle构建脚本同gradle交互的主要接口,几乎所有方法都在这个类中能找得到。

3.2 自定义task

Project提供了task函数方便我们创建task,先来看看Project中是如何定义task的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* <p>Creates a {@link Task} with the given name and adds it to this project. Calling this method is equivalent to
* calling {@link #task(java.util.Map, String)} with an empty options map.</p>
*
* <p>After the task is added to the project, it is made available as a property of the project, so that you can
* reference the task by name in your build file. See <a href="#properties">here</a> for more details</p>
*
* <p>If a task with the given name already exists in this project, an exception is thrown.</p>
*
* @param name The name of the task to be created
* @return The newly created task object
* @throws InvalidUserDataException If a task with the given name already exists in this project.
*/
Task task(String name) throws InvalidUserDataException;

简要描述就是,一旦task被创建,那么它会被当做project的一个属性,由gradle自动执行,Project有几个关于task的重载版本,功能大致一样。我们在Android中一般看的task是形如下面的格式:

1
2
3
4
5
task myTask // 创建一个名为myTask的task
task myTask { configure closure } // 重载版本,执行一段闭包
task myType << { task action } // 重载版本
task myTask(type: SomeType) // 创建task,并指定type,关于type,后面会讲到
task myTask(type: SomeType) { configure closure }

“<<”符号表示的task.doLast,”>>”表示task.doFirst,后面的闭包表示task执行完之后/之前,再执行这段闭包。

type:someType又是什么意思呢?其实就是告诉gradle,这个新建的task是从哪个基类task派生而来的。Gradle本身提供了一些通用的task,比如Copy,Delete等等。

1
2
3
4
5
6
7
public class Copy extends AbstractCopyTask {
...
}
public class Delete extends ConventionTask {
...
}

以一个例子来看看task的创建:

1
2
3
4
task clean(type: Delete) {
println "clean task executing."
delete rootProject.buildDir
}

这个例子在Top level的build.gradle中创建一个名为clean,type为delete的task,先来看看top level中tasks有什么变化:

此处输入图片的描述

可以看到,task一旦创建即被加入到project的task序列中,成为project的一个属性。

这个task具体做什么呢?task里面的这段闭包已经说明了,就是删除根目录下的build文件夹。task里面可以做很多事情,具体下面会介绍到。

3.3 task扩展

本小节将在新建task基础上做些扩展,来展示task是如何做更多事情的,相信大家看过之后,不再畏惧gradle,即使不会写但也至少能看得懂。

Android构建给我们已经提供了很多task(可以参考这里),这里结合Android中multidex实现来讲讲task的更多用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
tasks.whenTaskAdded { task -> // 遍历所有的task, "->"就是迭代器的意思
// 如果是create{Debug/Release}MainDexClassList,则执行以下闭包action.
if (task.name.startsWith("create") && task.name.endsWith("MainDexClassList")) {
task.doLast { // 该task执行完成之后,再执行以下action,替换分包内容.
File tempFile
File keepFile
if (task.name.contains("Debug")) {
tempFile = new File("$project.rootDir/MyProject/keep_in_maindexlist_debug.txt")
keepFile = new File("${project.buildDir}/intermediates/debug/maindexlist.txt")
} else if (task.name.contains("Release")) {
tempFile = new File("project.rootDir/MyProject/keep_in_maindexlist_release.txt")
keepFile = new File("{project.buildDir}/intermediates/release/maindexlist.txt")
}
tempFile.eachLine("utf-8") { str, linenumber ->
keepFile.append(str + "\n")
}
}
}
}

上述代码功能大概意思就是江create{Debug/Release}MainDexClassList这个task(对这个不熟的可以参考这里)形成的maindexkeep文件给替换掉。下面来具体分析这个例子:

(1) 首先这个在top level的build.gradle中,因为是要干预整个工程的分包,而不是针对某个module的;
(2) 这里的意思是将所有的task添加到project中,闭包有一个输入参数:task,这里”task ->”相当于迭代器,闭包内有一个默认参数it,代表的就是tasks,这里没有用到。
(3) 获取指定的task,可以根据getByName方法获取指定的task。

1
2
3
4
5
tasks.getByName("assembleRelease") {
it.doLast {
println "assemble release done."
}
}

除了上面提到的tasks.whenTaskAdded之外,我们还常见到project.afterEvaluated,它的意思是gradke解析完所有的task之后,并且在具体task开始执行之前,我们可以做的事情。

Android每个project都提供了tasks、project、gradle等内置对象供我们来访问,利用这些对象我们就可以对每个task进行hook,进而能够充分干预整个构建过程。