转载

Gradle tip #2: understanding syntax

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.

在第一篇博客中,我讲解了关于tasks和构建过程中task的不同阶段。在写完这篇之后,我意识到我应该更详尽的讲述一下Gradle。弄懂语法很重要,免得我们碰到复杂的构建脚本的时候直接晕菜。这篇文章我就会讲解一些语法上的东西。

Syntax

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.

Gradle脚本是使用Groovy语言来写的。Groovy的语法有点像Java,希望你能接受它。

If you feel comfortable with Groovy - feel free to skip this section.

如果你对Groovy已经很熟悉了,可以跳过这部分了。

There is one important Groovy aspect you need to understand in order to understand Gradle scripts - Closure.

Groovy中有一个很重要的概念你必要要弄懂–Closure(闭包)

Closures

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..

Closure是我们弄懂Gradle的关键。Closure是一段单独的代码块,它可以接收参数,返回值,也可以被赋值给变量。和Java中的Callable接口,Future类似,也像函数指针,你自己怎么方便理解都好。

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:

关键是这块代码会在你调用的时候执行,而不是在创建的时候。看一个Closure的例子:

def myClosure = { println 'Hello world!' }  //execute our closure myClosure()  #output: Hello world!

Or here is a closure which accepts a parameter:

下面是一个接收参数的Closure:

def myClosure = {String str -> println str }  //execute our closure myClosure('Hello world!')  #output: Hello world!

Or if closure accepts only 1 parameter, it can be referenced as it :

如果Closure只接收一个参数,可以使用it来引用这个参数:

def myClosure = {println it }  //execute our closure myClosure('Hello world!')  #output: Hello world!

Or if closure accepts multiple input parameters:

接收多个参数的Closure:

def myClosure = {String str, int num -> println "$str : $num" }  //execute our closure myClosure('my string', 21)  #output: my string : 21

By the way, argument types are optional, so example above can be simplified to:

另外,参数的类型是可选的,上面的例子可以简写成这样:

def myClosure = {str, num -> println "$str : $num" }  //execute our closure myClosure('my string', 21)  #output: my string : 21

One cool feature is that closure can reference variables from the current context (read class). By default, current context - is the class within this closure was created:

很酷的是Closure中可以使用当前上下文中的变量。默认情况下,当前的上下文就是closure被创建时所在的类:

def myVar = 'Hello World!' def myClosure = {println myVar} myClosure()  #output: Hello world!

Another cool feature is that current context for the closure can be changed by calling Closure#setDelegate() . This feature will become very important later:

另外一个很酷的点是closure的上下文是可以改变的,通过Closure#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!

As you can see, at the moment when we created closure, myVar variable doesn't exist. And this is perfectly fine - it should be present in the closure context at the point when we execute this closure.

正如你锁看见的,在创建closure的时候,myVar并不存在。这并没有什么问题,因为当我们执行closure的时候,在closure的上下文中,myVar是存在的。

In this case I modified current context for the closure right before I executed it, so myVar is available.

这个例子中。因为我在执行closure之前改变了它的上下文为m,因此myVar是存在的。

Pass closure as an argument

The real benefit of having closures - is an ability to pass closure to different methods which helps us to decouple execution logic.

closure的好处就是可以传递给不同的方法,这样可以帮助我们解耦执行逻辑。

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:

前面的例子中我已经展示了如何把closure传递给一个类的实例。下面我们将看一下各种接收closure作为参数的方法:

  1. method accepts 1 parameter - closure (只接收一个参数,且参数是closure的方法)

myMethod(myClosure)

  1. if method accepts only 1 parameter - parentheses can be omitted (如果方法只接收一个参数,括号可以省略)

myMethod myClosure

  1. I can create in-line closure (可以使用内联的closure)

myMethod {println 'Hello World'}

  1. method accepts 2 parameters (接收两个参数的方法)

myMethod(arg1, myClosure)

  1. or the same as '4', but closure is in-line (和4类似,单数closure是内联的)

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

  1. if last parameter is closure - it can be moved out of parentheses (如果最后一个参数是closure,它可以从小括号从拿出来)

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? ;)

这里我只想提醒你一下,3和6的写法是不是看起来很眼熟?

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:

现在我们已经了解了基本的语法了,那么如何在Gradle脚本中使用呢?先看下面的例子吧:

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

Look at that! Knowing Groovy syntax we can somewhat understand what is happening here!

知道了Groovy的语法,是不是上面的例子就很好理解了?

  • there is (somewhere) a  buildscript  method which accepts closure (首先就是一个buildscript方法,它接收一个closure):

def buildscript(Closure closure)

  • there is (somewhere) a  allprojects  method which accepts closure(接着是allprojects方法,它也接收一个closure参数):

def allprojects(Closure closure)

...and so on.

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.

现在看起来容易多了,但是还有一点不明白,那就是这些方法是在哪里定义的?答案就是Project

Project

This is a key for understanding Gradle scripts:

这是理解Gradle脚本的一个关键。

All top level statements within build script are delegated to Project instance(构建脚本顶层的语句块都会被委托给Project的实例)

This means that Project - is the starting point for all my searches.

这就说明Project正是我要找得地方。

This being said - let's try to find buildscript method.

If we search for buildscript - we will find  buildscript {}/ script block. But wait.. What the hell is script block??? According to documentation:

在Project的文档页面搜索buildscript方法,会找到buildscript{} script block(脚本块).等等,script block是什么鬼?根据文档:

A script block is a method call which takes a closure as a parameter(script block就是只接收closure作为参数的方法)

Ok! We found it! That's exactly what happens when we call buildscript { ... } - we execute method  buildscript which accepts Closure.

If we keep reading buildscript documentation - it says:  Delegates to: ScriptHandler from buildscript . It means that execution scope for the closure we pass as an input parameter will be changed to ScriptHandler. In our case we passed closure which executes repositories(Closure) and  dependencies(Closure) methods. Since closure is delegated to ScriptHandler , let's try to search for  dependencies method within  ScriptHandler class.

继续阅读buildscript的文档,文档上说Delegates to: ScriptHandler from buildscript。也就是说,我们传递给buildscript方法的closure,最终执行的上下文是ScriptHandler。在上面的例子中,我们的传递给buildscript的closure调用了repositories(closure)和dependencies(closure)方法。既然closure被委托给了ScriptHandler,那么我们就去ScriptHandler中寻找dependencies方法。

And here it is - void dependencies(Closure configureClosure), which according to documentation, configures dependencies for the script. Here we are seeing another terminology: Executes the given closure against the DependencyHandler . Which means  exactly the same as " delegates to [something] " - this closure will be executed in scope of another class (in our case -DependencyHandler)

找到了void dependencies(Closure configureClosure),根据文档,dependencies是用来配置脚本的依赖的。而dependencies最终又是委托到了DependencyHandler。

" delegates to [something] " and " configures [something] " - 2 statements which mean exactly the same - closure will be execute against specified class.

Gradle extensively uses this delegation strategy, so it is really important to understand terminology here.

看到了Gradles是多么广泛的使用委托了吧。理解委托是很重要的。

For the sake of completeness, let's see what is happening when we execute closure {classpath 'com.android.tools.build:gradle:1.2.3'} within  DependencyHandler context. According to documentation this class configures dependencies for given configuration and the syntax should be:

So with our closure we are configuring configuration with name classpath to use com.android.tools.build:gradle:1.2.3 as a dependency.

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!

默认情况下,Project中预先定义了很多script block,但是Gradle插件允许我们自己定义新的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. 

这就意味着,如果你在build脚本顶层发了一些{…},但是你在Gradle的文档中却找不到这个script blocks或者方法,绝大多情况下,这是一些来自插件中定义的script block。

android Script block

Let's take a look at the default Android app/build.gradle build script:

我们来看看默认的Android app/build.gradle文件:

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 :)

可以看到,这里有一个android方法,它接收一个closure参数。但是如果我们在Project的文档中搜索,是找不到这个方法的。原因很简单,这不是在Project中定义的方法。

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 class 1 ).

仔细查看build脚本,可以看到在抵用android方法之前,我们使用了com.android.application插件。Android application插件扩展了Project对象,添加了android Script block。

But where can I find Android plugin documentation? And the answer is - you can download documentation from the official Android Tools website.

哪里可以找到Android插件的文档呢?可以从Android Tools website查找。

If we open AppExtension documentation - we will find all the methods and attributes from our build script:

如果我们打开AppExtension的文档,我们就可以找打build script中使用的方法了属性:

  1. compileSdkVersion 22 . if we search for  compileSdkVersion  we will find property. In this case we assign  "22"  to property  compileSdkVersion (compileSdkVersion 22. 如果我们搜索compileSdkVersion,将会找到这个属性. 这里我们给这个属性赋值 “22”)
  2. the same story with  buildToolsVersion (buildToolsVersion和1类似 )
  3. defaultConfig  - is a script block which delegates execution to ProductFlavor  class (defaultConfig - 是一个script block将会委托给ProductFlavor类来执行。 )
  4. .....and so on

So now we have really powerful ability to understand the syntax of Gradle build scripts and search for documentation.

现在我们已经能够理解Gradle脚本的语法了,也知道如何查找文档了。

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!

在AppExtension中,我找了一个script block testOptions,它会把closure参数委托给TestOptions类。根据文档,TestOptions类有两个属性:reportDir和resultsDir。reportDir是用来保存test报告的。我们来改改这个属性试试。

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

Here I used rootDir property from  Project class which points to the root project directory.

这里我使用了Project中定义的rootDir属性,该属性值为项目的根目录。

So now if I execute ./gradlew connectedCheck , my test report will go into [rootProject]/test_reports directory.

现在如果我执行./gradlew connectedCheck,test报告将会被保存到[rootProject]/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.

不要在真实的项目尝试这个修改,所有的构建输出都应该在build目录中,这样才不至于污染你的项目目录结构。

Happy gradling!

我是天王盖地虎的分割线

http://trickyandroid.com/gradle-tip-2-understanding-syntax/

http://blog.csdn.net/lzyzsd/article/details/46935063

正文到此结束
Loading...