作为 Android 开发者必须了解的 Gradle 知识 (译)

本文转载自:https://juejin.im/post/58ca92192f301e007e39be9d

wutongke

16小时前

作为 Android 开发者必须了解的 Gradle 知识 (译)

在Android开发中,很多时候我们不需要修改 *.gradle 文件太多,我们添加依赖、修改target compile、最低支持API level,或者修改签名配置和build类型。其它更复杂一些逻辑,我们最后可能就是从Stack Overflow中copy了一些自己也不太懂的代码。本文中我们将一步一步介绍Android工程中用到的gradle文件及其背后的原理。

1. Groovy

1.1 语法

Gradle文件其实是用Groovy脚本写的,我们都会写java,所以入门Groovy非常简单。首先我们需要了解一下几点:

1.调用至少包含一个参数的方法时不需要使用括号:

def printAge(Stringname, int age) {    print("$name is $age years old")}def printEmptyLine() {    println()}def callClosure(Closure closure) {    closure()}printAge"John",24// Will print "John is 24 years old"printEmptyLine()// Will, well, print empty linecallClosure { println("From closure") }// Will print "From closure"

2.如果方法的最后一个参数是闭包(或者说是lambda表达式),可以写在括号外(注:这个特性很重要,gradle文件中的很多配置其实都是参数为闭包的方法):

def callWithParam(Stringparam, Closure closure) {    closure(param)}callWithParam("param", { println it })// Will print "param"callWithParam("param") { println it }// Will print "param"callWithParam"param", { println it }// Will print "param"

3.对于Groovy方法中命名过的参数,会被转移到一个map中做为方法的第一个参数,那些没有命名的参数则加在参数列表之后:

def printPersonInfo(Map person) {    println("${person.name} is ${person.age} years old")}def printJobInfo(Map job,StringemployeeName) {    println("$employeeName works as ${job.title} at ${job.company}")}printPersonInfo name:"John",age:24printJobInfo"John",title:"Android developer",company:"Tooploox"

这段程序会打印“John is 24 years old”和“John works as Android developer at Tooploox”,方法调用的参数可以是乱序的,map会被作为第一个参数传入!这里的方法调用也省略了括号。

1.2 闭包

闭包是一个非常重要的特性,需要解释一下。闭包可以理解为lambada。他们是一段可以被执行的代码,可以有参数列表和返回值。我们可以改变一个闭包的委托:

classWriterOne{    def printText(str) {        println"Printed in One: $str"}}classWriterTwo{    def printText(str) {        println"Printed in Two: $str"}}def printClosure = {    printText"I come from a closure"}printClosure.delegate =newWriterOne()printClosure()// will print "Printed in One: I come from a closureprintClosure.delegate =newWriterTwo()printClosure()// will print "Printed in Two: I come from a closure

我们可以看到printClosure调用了不同委托的printText方法,之后会解析这个特性在gradle中的重要性。

2. Gradle

2.1 脚本文件

有三个主要的gradle脚本,每个都是一个代码块。

build.gradle 文件,针对Project对象

settings.gradle文件,针对Settings对象

全局配置的初始化gradle脚本,针对Gradle实例

2.2 Projects

gradle 构建一般包含多个Project(在Android中每个module对应这里的project),project中包含tasks。一般至少有一个root project,包含很多subprojects,subproject也可以嵌套project(注:Android 中对应每个library module还可以依赖其它library module)。

3. 构建基于Gradle的Android工程

Android工程中我们一般有如下的结构:

1是root project的setting文件,被Settings执行

2是root project的build配置

3是App project的属性文件,会被注入到 App的Settings中

4是App project的build配置

3.1 创建gradle工程

我们新建一个文件夹,命名为example,cd进入后执行gradle projects命令,之后就已经拥有一个gradle project了:

$ gradle projects:projects------------------------------------------------------------Root project------------------------------------------------------------Root project'example'No sub-projectsTo see a listofthe tasksofa project, run gradle :tasksFor example,tryrunning gradle :tasksBUILD SUCCESSFULTotal time:0.741secs

3.2 配置projects层级

如果我们要建立一个默认的Android project(空的root project和一个包含Application的app project),我们就需要配置settings.gradle,the documentation中介绍settings.gradle:

声明需要实例化的配置和build的project的层级体系配置

我们通过void include(String[] projectPaths)方法来添加projects:

这里的冒号:用于分隔子project,可以参考这里here。因此我们在这里写:app, 而不是直接写app。

在settings.gradle中写rootProject.name = <>也是一个比较好的实践。如果没有写,那么root project 的默认名字就是project所在文件夹的名字。

3.3 配置Android 子project

我们已经配置了root project的build.gradle,现在来看看如何配置Android project。

user guide可以知道我们首先要为app project配置com.android.application插件,我们来看看apply方法:

voidapply(Closure closure)voidapply(Map options)voidapply(Action action)

尽管第三个方法很重要,我们通常使用是第二个方法,它用到我们之前提到的特性,通过map来传递参数。通过文档我们可以查看可以使用哪些参数:

voidapply(Map(options)

以下是可用的参数:

from: 可以引入一个脚本apply(...),如apply from: "bintray.gradle"从而导入一个可用脚本。

plugin: apply的plugin的id或者实现类

to: 委托目标

我们知道需要传递一个id值作为plugin的参数,可以写作:apply(plugin:'com.android.application'),这里的括号也可以省略,我们在app的build.gradle中配置:

命令行中运行:

报错了,找不到com.android.application的定义,这不奇怪,我们并没有配置,但是gradle是如何查找Android的plugin jar包呢?在user guide可以找到答案,我们需要配置plugin的路径。

现在我们可以在root project或者app的build.gradle中配置路径,但是因为buildscript闭包是ScriptHandler执行的,其它子project也需要使用,因此最好配置在root project的build.gradle中:

buildscript {    repositories {        jcenter()    }    dependencies {        classpath'com.android.tools.build:gradle:2.3.0-beta2'}}

如果我们在上边的代码中添加括号,那么就会发现其实都是带有闭包参数的方法调用。如果我们研究下文档,我们就可以知道是有哪些对象执行这些闭包的,总结如下:

buildscript(Closure)是Project实例中调用的,传递的闭包的由ScriptHandler执行

repositories(Closure)是在ScriptHandler实例中调用,传递的闭包由RepositoryHandler执行

dependencies(Closure)是在ScriptHandler实例中调用,传递的闭包由DependencyHandler执行。

也就是说jcenter()是由RepositoryHandler调用

classpath(String)是由DependencyHandler(*)调用

译者注:如果这里看不懂的同学,可以再回头看看groovy的语法部分,其实这里上边的代码都是方法,如buildscript是Project的方法,我们知道groovy语法中如果最后一个参数是闭包的话,可以不写括号。

如果查看DependencyHandler的代码,我们会发现其实没有classpath这个方法,这是一种特殊的调用,我们在稍后讨论。

3.4 配置Android subproject

如果我们现在执行Gradle task,依然有错误:

显然,我们还没有设置Android相关的配置,但是我们的Android plugin已经可以被正确apply了,我们增加一些配置:

android {  buildToolsVersion"25.0.1"compileSdkVersion25}

到这里我们知道,android方法被加入到了Project实例中,闭包传递给了delegate(这里是AppExtension),定义了buildToolsVersion和compileSdkVersion方法,Android plugin使用这种方式接收所有的配置,包括default configuration,flavors等等。

想要执行gradle task,还需要两个文件:AndroidManifest.xml和local.properties,local.properties中配置sdk.dir,(或者在系统环境中配置ANDROID_HOME),指向Android SDK的位置。

3.5 扩展

android方法是如何出现在Project实例中的呢,还有我们的build.gradle是怎样被执行的?简单的说,Android plugin 用android这个名字注册AppExtension类为extension。这个超出了本文的范围,但是我们要知道Gradle可以为每一个注册过的 plugin增加闭包配置。

3.6 依赖

还有一个重要的部分,dependencies还没有讨论:

dependencies {    compile'io.reactivex.rxjava2:rxjava:2.0.4'testCompile'junit:junit:4.12'annotationProcessor'org.parceler:parceler:1.1.6'}

为什么这里特殊呢,因为如果查看DependencyHandler,也就是执行这个闭包的委托,它是没有compile,testCompile等方法的。这个问题是有意义的,如果我们随意增加一个freeCompile 'somelib',可以吗?DependencyHandler不会定义所有的方法,其实这里涉及到Groory语音的另一个特性:methodMissing,这允许在运行时catch对于未定义方法的调用。

实际上Gradle使用了MethodMixIn中声明的methodMissing,类似的机制在为定义的属性中也是一样的。

相关的dependency操作可以在这里找到,它的行为如下:

如果未定义方法的调用方有至少一个参数,如果存在configuration()与被调用方法有相同的名字,那么就根据参数的类型和数量,调用具有相关参数的doAdd方法。

每个plugin都可以增进configuration到dependencies handler中,如Android插件增加了compile, compileClasspath, testCompile和一些其它配置here,Android 插件还增加了annotationProcessor配置,根据不同build类型和产品形式还有Compile, TestCompile等等。

由于doAdd是私有方法,一次这里调用的是公有的add方法,我们可以重写上边的代码,但最后不要这样做:

dependencies {    add('compile','io.reactivex.rxjava2:rxjava:2.0.4')    add('testCompile','junit:junit:4.12')    add('annotationProcessor','org.parceler:parceler:1.1.6')}

3.7 Flavors, build types, signing configs

我们看以下代码:

productFlavors {    prod {    }    dev {        minSdkVersion21multiDexEnabledtrue}}

如果我们查看源码,可以发现productFlavors是这样声明的:

voidproductFlavors(Action> action) {    action.execute(productFlavors)    }

Action是Gradle中定义的由T执行的闭包

所有这里我们有了NamedDomainObjectContainer,NamedDomainObjectContainer可以创建和配置多个ProductFlavorDsl类型的对象,并根据ProductFlavorDsl的名字保存ProductFlavorDsl。

这个容器可以使用动态方法创建指定类型的对象(这里的ProductFlavorDsl),并和名字一起存放在容器中,所以当我们使用{}参数调用prod方法时,他被productFlavors实例执行,执行说明如下:

NamedDomainObjectContainer获取到被调用方法的名字,生成ProductFlavorDsl对象,配置给定的闭包,保存方法名字到新的配置ProductFlavorDsl的映射。

Android plugin可以从productFlavors中获取ProductFlavorDsl,我们可以把它作为属性进行访问:productFlavors.dev,这样我们就可以拿到名字为dev的ProductFlavorDsl,这也是我们可以写signingConfigsigningConfigs.debug的原因。

4. 总结

对于Android开发者来说,Gradle文件是非常常用的,并不是什么黑魔法。但是Gradle有很多约定,而且使用Groovy语言也增加了一些复杂性,知道这两点,Gradle并不是什么魔法。希望了解通过这篇文章介绍的内容,即使是从stackoverflow中粘贴代码,也能知道它背后的意义。

这是一篇译文,原文作者对Android的gradle进行了比较深入的介绍,希望各位同学可以真正了解我们常用的gradle文件背后的原理,而不仅仅是简单地配置gralde。文中有些不太容易理解的地方,可以根据文中给出的链接了解更多内容。

原文地址https://medium.com/@wasyl/understanding-android-gradle-build-files-e4b45b73cc4c#.svvmjs12o

推荐阅读:

重要-作为Android开发者必须了解的Gradle知识

编写高效的Android代码(译)

Android中使用gradient的一条建议

寻找卓越的(Android)软件工程师

gradle

GitHub

程序员

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

推荐阅读更多精彩内容