Just Enough Gradle for Android

前言

做Android开发的没有不知道Gradle的。时不常地去build.gradle中添加一些dependencies,或者修改一些诸如versionCoce,versionName之类的属性都已经是家常便饭。然而基本上也就仅限于此。我们都知道,Gradle是基于Groovy语言的,得益于Groovy语言的简洁与直白,应付所谓的“家常便饭”根本不需要额外的学习。然而,作为一名优秀的“程序猿”,我时常好奇,Gradle到底是啥,如此简洁的背后是不是隐藏着什么大秘密。于是我决定稍微深入地学习一下Gradle。

先抛出结论,经过我这段时间的学习,对Gradle背后的秘密有了一定的了解。然而,理解了这些之后,我对Gradle的使用仍然没有超过所谓“家常便饭”的级别。如果你秉持着能用就好的原则,那么现在你就可以把这篇文章关掉了。余下的内容只是对Gradle的释惑,让我们在使用Gradle时不再只是“能用”,而是“够用”。不过也仅仅是够用。Just Enough Gradle for Android...

Just Enough Groovy for Gradle

众所周知,Gradle是使用Groovy语言编写的(不过现在也有Kotlin版本的),我们先学习一点Groovy的基本语法。

//变量定义
def x = 3
//也可以传统点
int x = 3

//函数定义
String testFunction(def arg1) {//无需指定参数类型,当然指定也可以
    return ""
}

//无类型的函数定义,必须使用 def 关键字
def nonReturnTypeFunc() {
    last_line //return可以省略,如果没有return,最后一行代码的执行结果就是本函数的返回值
}
//也可以传统点
String getString() {
    return "I am a string"
}

//函数调用可以不加括号
println("Hello, Groovy!")
println "Hello, Groovy!"
//没有参数的函数,还是要加上()的,不然谁知道是函数还是变量,例如上方的getString函数调用
getString()

//字符串,单引号的字符串完全是字面值;双引号字符串,有字符串插入
String name = 'Groovy'
assert "Hello, ${name}!" == 'Hello, Groovy!'
//在不引起混淆的时候可以简写
assert "Hello, $name!" == 'Hello, Groovy!'

//List定义
def nums = [1, 2, 3]
assert nums[0] == 1
assert nums[-1] == 3 //倒数第一个
//Map定义
def map = [a:1, b:2, c:3]
assert map.a == 1
assert map['b'] == 2
assert map.get('c') == 3
//List、Map上都定义了诸如each,all,find等函数
nums.each {
    //隐含一个参数 it
    println it
}

//闭包作为函数的最后一个参数,可以写到括号外面
map.each ({
    println "key=${it.key},value=${it.value}"
})
//等价于
map.each() {
    println "key=${it.key},value=${it.value}"
}
//括号省略
map.each {
    println "key=${it.key},value=${it.value}"
}


//闭包closure,类似于Java 8和Kotlin中的lambda表达式,不同的是在Groovy中闭包都属于一个叫Closure的类
//这就有个问题,如何知道当前闭包的参数有几个,类型是啥,这就有赖于你对API的熟悉程度
public static <T> List<T> each(List<T> self, Closure closure)

Gradle概览

问:码农最重要的能力是什么?答:打码,噢,不对,码代码。但是除了码代码之外,每个码农还必须会把代码部署完成。对于Android来说就是打包。而Gradle就是帮我们做这一系列事情的工具,也就是构建工具。

在Android Studio中一个总工程称为一个Project,每个Project可以包含一个或多个模块,称之为Module,比如说最常见的app Module。在Gradle中,每个build.gradle文件都会生成一个Project对象,顶层build.gradle生成的Project称为RootProject,各个module中的build.gradle生成的Project的称为SubProject。

Gradle中的每一个Project都包含一系列Task,一个具体的编译过程是由一个个的 Task 来定义和执行的。比如 Android APP Project 包含源码编译Task、资源编译Task、lint检查Task、测试Task、打包Task、签名Task等等。一个Project包含有多少Task,主要是由插件决定的。而插件就是用来定义Task的。例如编译Java有Java插件,编译Android APP 有 Android Appliaction 插件,编译 Android Library 有 Android Library 插件。

//app的 build.gradle 指定了android application插件
apply plugin: 'com.android.application'

//module的 build.gradle 指定了android library插件
apply plugin: 'com.android.library'

//纯java工程的 build.gradle 指定了java插件
apply plugin: 'java'

每一个build.gradle对应了一个Project,这个Project包含了哪些Task主要由我们指定的插件决定。

至此,我们知道,编译过程即Task执行过程。不过还有个问题,以Android APP的编译为例,打包Task不是孤立的,它必须依赖于诸如源码编译Task等的执行,也就是说必须先执行源码编译Task等一系列Task,才能执行打包Task。这就是Task之间的依赖。每个Project中的Task会在配置阶段,根据彼此之间的依赖关系构成一个有向无环图(DAG)。这个DAG决定了Task的执行顺序。下图展示了Java插件的Tasks之间的依赖关系(本来想找Android 插件的Tasks,但是没有找到,可能是因为Android很多Task是动态生成的,Tasks不确定):

JavaPluginTasks

如上图,箭头方向即代表了依赖关系。例如 build task 依赖于 check task 和 assemble task,也就是说必须先执行check 和 assemble,才能执行build。以此类推。例如我们执行build task

> gradlew build
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar
:assemble
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:build
BUILD SUCCESSFUL
Total time: 1.956 secs

可以看出依赖链上的所有task被依次执行。定义task之间的依赖关系也很多简单:

//定义两个task,hello 和 world,后面会有Task的更详细的介绍
task hello << {
    println 'hello'
}
task world << {
    println 'world'
}

world.depensOn hello

//或者task定义的时候就指定
task world(dependsOn: hello) << {
    println 'world'
}

Gradle深入

Gradle工作流程

Gradle 工作包含三个阶段:

  1. 初始化阶段。读取gradle.properties中的参数;执行settings.gradle,确定共有多少个Project。
  2. 配置阶段。解析每个 project 中的 build.gradle,根据Taks之间的依赖关系构建有向无环图,确定Task的执行顺序。
  3. 执行阶段。

如图所示,在每个阶段结束之后,我们都可以添加一些Hook,完成我们的一些目的。

Gradle 主要有三种对象,这三种对象和三种不同的脚本文件对应。

  1. Gradle 对象:在整个执行过程中,只有这么一个对象。Gradle对象的数据类型就是 Gradle。我们一般很少去配置这个对象。
  2. Project 对象:每一个 build.gradle 会转换成一个 Project 对象。
  3. Settings 对象:settings.gradle 会转换成一个 Settings 对象。

其中最重要的是Project对象。build.gradle 是每个Project构建的入口,在这个文件我们主要:

  • 加载插件
  • 设置属性、配置插件(例如,版本号,依赖配置等等)
  • 自定义Task(独立的,或者依赖于别的Task)
  • 改变已有Task的行为(一般是增加一些我们自定义的行为)

一个常见的问题是,我们的工程往往不止一个build.gradle 文件,也即不止一个Project对象,怎么能让这些对象“共享”一些属性呢?Gradle 提供了一种名为 extra property(额外属性) 的方式。extra property 支持Project 和 Gradle 对象。

例如在Android Studio中创建一个支持Kotlin的项目,则其顶层的 build.gradle 如下:

buildscript {
    //额外属性
    ext.kotlin_version = '1.2.41'
    //也可以这样写
    ext {
        kotlin_version = '1.2.41'
        //更多的额外属性
        ...
    }
    
    repositories {
        google()
        jcenter()
    }
    
    dependencies {
        classpath 'com.android.tools.build:gradle:3.1.2'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}

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

以如上这种方式定义额外属性,是在RootProject对象上定义了额外属性,而所有的SubProject都会“继承”RootProject的额外属性。这样就实现了在多个Project对象“共享”属性。

真正有用的知识

上面扯了那么多,不禁让人疑问,有毛用?!确实是用处不大。看来,是时候展现真正的技术了。

先来一波总结:

Gradle流程

很明显,关隘就在于Task。前面说过Task主要是由插件定义的,当然我们也可以定义自己的Task,完成我们的目的。对于Gradle的使用,主要在两个方面,首先是对插件定义好的属性地配置,这就要求我们了解插件定义了哪些属性,这些属性起什么作用;其次是自定义Task,完成我们特定的目的。

定义Task的几种方式

task hello {
    doFirst {
        println 'hello'
    }
    doLast {
        println 'world'
    }
}

task('hello') {
    doFirst {
        println 'hello'
    }
    doLast {
        println 'world'
    }
}

//tasks是project中的一个属性
tasks.create('hello') {
    doFirst {
        println 'hello'
    }
    doLast {
        println 'world'
    }
}

//没啥用的task
task hello {
    println 'hello'
}

前面说过,Gradle有三个阶段初始化、配置和执行。如果我们像最后一种方式定义一个Task的话,那么闭包中的代码将在配置的时候执行,这没啥卵用,也不是我们定义Task的目的。
当我们执行一个Task的时候,其实是执行其拥有的actions列表,这个列表保存在Task对象实例中的actions成员变量中
private List<ContextAwareTaskAction> actions = new ArrayList<>()
所以说,Task中的doFirst和doLast方法只是向actions中添加Action,这样才能在执行阶段执行。

task hello {
    //doFirst并不常用
    doLast {
        println 'world'
    }
}

//可以简写, << 运算符的重载,doLast 的简写
task hello << {//这里即是一个Action的闭包
    println 'world'
}

//不必从头定义每个task,Gradle已经帮我们定义好了很多常用的Task,我们只需要配置一下就能用了
//如下是一个删除task,要删除的是根目录下的build目录
task clean(type: Delete) {
    delete rootProject.buildDir
}

//或者一个拷贝task
task copyOutputs(type: Copy) {
    from "$buildDir/outputs/apk"
    into '../results'
}

groovy 支持运算符重载,Task类上 << 的重载就是一个很好的例子。选择这个运算符进行重载还挺形象的,由左移引申为向actions列表末尾添加。

小结

至此,我们基本上对Gradle有了一个大致的了解。Gradle是一个构建工具,利用插件帮我们定义了许多Task,执行这些Task(其实是Task中的Action)完成构建任务。我们需要做的就是:首先,配置插件的各种属性,方便、快捷,能满足我们大部分的需求;其次,如果配置属性没有办法满足我们的要求,我们可以自定义Task,达到我们的目的。这篇文章主要是对Gradle知识性地介绍,想看更多Gradle在Android中应用的实例,请看Android Gradle 多维度实例

参考书籍

Gradle for Android
Gradle Recipes for Android
《深入理解Android之Gradle》
《Android Gradle权威指南》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,424评论 25 707
  • 说明 本文主要从实现原理和代码层面介绍Gradle开发相关知识。关于本文中提到的、Gradle中的基本概念等内容,...
    jzj1993阅读 7,872评论 1 33
  • 参考资料:http://gold.xitu.io/post/580c85768ac247005b5472f9htt...
    zhaoyubetter阅读 10,977评论 0 6
  • 每天认真卸妆洗脸冲澡,多读书,按时睡,少食多餐,变得温柔,大度,继续善良,保持爱心,不在人前矫情,四处诉说以求宽慰...
    笃学青衿阅读 220评论 0 0
  • 今天来新疆整一个月了,新疆的一个多月时间里,生活紧张,但又有充实。刚进入这个行业,一切都是陌生而又崭新的,所有的事...
    直播中阅读 190评论 0 0