gradle 技巧之语法浅谈

发布于 2024-12-12 07:40:32 字数 9854 浏览 15 评论 0

In the Part 1 we talked about tasks and different stages of the build lifecycle. But after I published it I realized that before we jump into Gradle specifics it is very important to understand what we are dealing with - understand its syntax and stop being scared when we see complex build.gradle scripts. With this article I will try to fill this missing gap.

在上一篇的博文(Gradle tip #2 : Tasks) 中,我们讨论了 gradle 构建的基本单位 Task。并且介绍了构建过程的各个阶段及其生命周期。而本文会重点介绍 gradle 的语法。只有具备了 gradle 的相关语法知识,才会大幅度的提高对于阅读、学习或者编写 gradle 脚本的效率,正所谓 磨刀不误砍柴工 是也。

引言

Gradle build scripts are written in Groovy, so before we start analyzing them, I want to touch (briefly) some key Groovy concepts. Groovy syntax is somewhat similar to Java, so hopefully you won't have much problems understanding it. If you feel comfortable with Groovy - feel free to skip this section.There is one important Groovy aspect you need to understand in order to understand Gradle scripts - Closure.

gradle 是 groovy 语言实现的构建工具. groovy 是运行在 jvm 平台的一门敏捷开发语言.其语法和 java 有诸多类似之处,然而其具备一些 java 没有的概念需要读者细细体会。下面会详细的介绍 groovy 的基本语法,当然如果您已经对 groovy 的语法有了一定的了解.可以直接跳过这一小节。

1、闭包的基本语法

Closure is a key concept which we need to grasp to better understand Gradle. Closure is a standalone block of code which can take arguments, return values and be assigned to a variable. It is some sort of a mix between Callable interface, Future, function pointer, you name it.. Essentially this is a block of code which is executed when you call it, not when you create it. Let's see a simple Closure example:

闭包是 groovy 中最重要的概念之一. 简单地说闭包(Closures) 是一段代码块. 这个代码块可以接受参数并具有返回值. 有一点要非常注意的是, 闭包往往不是在需要使用的时候才写出来 这么一段代码(就像 Java 的匿名类那样), 通过 def 关键字可以声明一个变量代表一个闭包,然后在需要的时候直接使用该变量即可,多说无益,请看如下的例子:

一个简单的 hello world 闭包:
def myClosure = { println 'Hello world!' }
// execute our closure myClosure()
output: Hello world!
如下是一个接受参数的闭包的例子:
def myClosure = {String str -> println str }
// execute our closure myClosure('Hello world!')
output: Hello world!
如果闭包只接受一个参数,这个参数在代码块中可以用 it 代替:
def myClosure = {println it }
//execute our closure myClosure('Hello world!')
output: Hello world!
如下是接受多个参数的闭包的例子:
def myClosure = {String str, int num -> println "$str : $num" }
// execute our closure myClosure('my string', 21)
output: my string : 21
闭包里面的参数类型可以省略不写,例子如下:
def myClosure = {str, num -> println "$str : $num" }
//execute our closure myClosure('my string', 21)
output: my string : 21
闭包还有一个比较酷的写法就是,可以直接调用 context 里面的变量,默认的 context 就是创建这个闭包的类(class) 例子如下:
def myVar = 'Hello World!' def myClosure = {println myVar} myClosure()
output: Hello world!
上面提到了闭包可以直接调用 context 的变量,这个 context 可以通过 setDelegate() 方法来改变,极大的增加了闭包的灵活性!
def myClosure = {println myVar}
//I'm referencing myVar from MyClass class MyClass m = new MyClass() myClosure.setDelegate(m) myClosure()
class MyClass { def myVar = 'Hello from MyClass!' }
output: Hello from MyClass!

2、闭包可以作为参数进行传递

The real benefit of having closures - is an ability to pass closure to different methods which helps us to decouple execution logic. In previous section we already used this feature when passed closure to another class instance. Now we will go through different ways to call method which accepts closure:

在 groovy 中,将闭包作为参数传递进函数,是将逻辑进行分离解耦的重要手段.在上述的例子中,我们已经尝试了如何将闭包作为参数进行传递.下面我们总结一下传递闭包的方法:

1 接受一个参数的函数
myMethod(myClosure)
2 如果函数只接受一个参数,括号可以忽略

myMethod myClosure

3 可以将闭包以插入语的形式创建

myMethod {println 'Hello World'}

4 函数接受两个参数

myMethod(arg1, myClosure)

5 接受两个参数,同样可以用插入语创建闭包

myMethod(arg1, { println 'Hello World' })

6 如果存在多个参数,且最后一个参数是闭包,闭包可以不写在括号内
myMethod(arg1) { println 'Hello World' }

At this point I really have to point your attention to example #3 and #6. Doesn't it remind you something from gradle scripts?

细心的朋友们是不是觉得上述的六种用法中,第三条和第六条很眼熟?很像 gradle 中的 scripts 了?

Gradle

Now we know mechanics, but how it is related to actual Gradle scripts? Let's take simple Gradle script as an example and try to understand it:

在知道了 groovy 的基本语法(尤其是闭包) 之后,下面我们就以一个简单的 gradle 脚本作为例子具体感受一下:

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

allprojects { repositories { jcenter() } }

结合前文的例子,我们可以很容易的理解到,buildscript 是一个接受闭包作为参数的函数,这个函数会在编译的时候被 gradle 调用.这个函数的定义就类似于:def buildscript(Closure closure). 而 allprojects 同理也是一个接受闭包作为参数的函数.

This is cool, but this information alone is not particularly helpful... What does "somewhere" mean? We need to know exactly where this method is declared.

那么问题来了,这些函数具体会在什么时候被 gradle 调用呢?要回答这个问题就需要介绍另一个知识点:Project

Project

在这里,我觉得逐句翻译作者查阅文档的步骤没有太大的意义,我自己总结了一下作者的概念如下:

理解 gradle 配置文件中的 script 如何调用的关键就是理解 project 的相关概念.在 gradle 执行某个"任务"的时候,会按照各个 task 的依赖关系来依次执行. 而执行这些 task 的对象就是 Project.说的在通俗一些,project 就是你希望 gradle 为你做的事情,而要完成这些事情,需要将事情分成步骤一步一步的做,这些步骤就是 task.

Script blocks

By default, there is a set of pre-defined script blocks within Project, but Gradle plugins are allowed to add new script blocks!

It means that if you are seeing something like something { ... } at the top level of your build script and you couldn't find neither script block or method which accepts closure in the documentation - most likely some plugin which you applied added this script block.

通过前文的学习,我们已经很清楚的了解到 scipt block 就是一段接受闭包的函数,这些函数会被 Project 调用,默认的情况下,gradle 已经准备 好了很多 script 用于我们对项目进行配置,例如 buildScript{} ... ... 当然你也可以自己写出符合规范的 task 来在编译的过程中被调用.

下面我们先看一下 Android Studio 中默认的 script:

apply plugin: 'com.android.application'

android { compileSdkVersion 22 buildToolsVersion "22.0.1"

defaultConfig {
    applicationId "com.trickyandroid.testapp"
    minSdkVersion 16
    targetSdkVersion 22
    versionCode 1
    versionName "1.0"
}
buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}

}

As we can see, it seems like there should be android method which accepts Closure as a parameter. But if we try to search for such method in Project documentation - we won't find any. And the reason for that is simple - there is no such method :)

If you look closely to the build script - you can see that before we execute android method - we apply com.android.application plugin! And that's the answer! Android application plugin extends Project object with android script block (which is simply a method which accepts Closure and delegates it to AppExtension class1).

按照我们已经有的知识,上面的脚本说明有一个名称为 android 的函数,该函数接收闭包作为参数,然而其实在 Gradle 的文档中是不存在这个函数 的. 那么 android 脚本怎么会出现在这里呢? 答案就是最上面的 apply plugin: 'com.android.application'.这个插件提供了 android 构建所需要的各种 script.

But where can I find Android plugin documentation? And the answer is - you can download documentation from the official Android Tools website (or here is a direct link to documentation).

既然 gradle 官方的文档中没有 android 相关的 script 信息,那我们该怎么查阅呢? 您可以去官方的 android 网站上查阅,如果懒得找的话请点击这个链接:https://developer.android.com/shareables/sdk-tools/android-gradle-plugin-dsl.zip

If we open AppExtension documentation - we will find all the methods and attributes from our build script: compileSdkVersion 22. if we search for compileSdkVersion we will find property. In this case we assign "22" to property compileSdkVersion the same story with buildToolsVersion defaultConfig - is a script block which delegates execution to ProductFlavor class .....and so on So now we have really powerful ability to understand the syntax of Gradle build scripts and search for documentation.

您下载了前文连接的文档后,可以发现有一个 html 格式的文档的名字是 AppExtension, 这个文档主要就是介绍了 Android configuration blocks. 即在 gradle 官方文档中没有的关于 Android 配置的各种 gradle script 都可以在这里进行查阅(几个例子): 1、 compileSdkVersion 在文档中的描述是 Required. Compile SDK version. 即这个脚本是 gradle 进行 Android 构建之必需,并且这个脚本是 是用来描述编译的时候使用的 sdk 版本. 2、buildToolsVersion 在文档中的描述是 Required. Version of the build tools to use. 即该脚本是构建之必需,其用于告诉 gradle 使用 哪个版本的 build tools 3 ... ... (详细情况请参阅文档吧:))

Exercise

With this powerful ability (oh, that's sounds awesome), let's go ahead and try reconfigure something :) In AppExtension I found script block testOptions which delegates Closure to TestOptions class. Going to TestOptions class we can see that there are 2 properties: reportDir and resultsDir. According to documentation, reportDir is responsible for test report location. Let's change it!

有了前文的学习作为基础,我们已经了解了 gradle 语法以及 android 插件的脚本查阅方法. 那么接下来我们实际运用这些知识,自定义的对我们 的 Android 项目进行一些配置. 在上述的 AppExtension 文档中,我查阅到了一个脚本的名字是 testOptions. 这段脚本代表的是 TestOption class 调用,TestOption class 里有三个属性:reportDir、resultsDir 和 unitTests. 而 reportDir 就是测试报告最后保存的位置,我们现在就来改一下 这个地方.

android { ...... testOptions { reportDir "$rootDir/test_reports" } }

Here I used rootDir property from Project class which points to the root project directory. So now if I execute ./gradlew connectedCheck, my test report will go into [rootProject]/test_reports directory.

在这里,我使用了"$rootDir/test_reports"作为测试结果的储存位置, $root 指向的就是项目的根目录.现在如果我通过命令行执行 ./gradlew connectedCheck. gradle 就会进行一系列的测试程序并且将测试报告保存在项目根目录下的 test_reports 文件中.

Please don't do this in your real project - all build artifacts should go into build dir, so you don't pollute your project structure.

注意一点的是,这个关于测试的小例子,别用在你真是的生产环境中,尽量保持你项目结构的 清洁。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

文章
评论
27 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文