在第一篇部落格中,我講解了關於tasks和構建過程中task的不同階段。在寫完這篇之後,我意識到我應該更詳盡的講述一下Gradle。弄懂語法很重要,免得我們碰到複雜的構建指令碼的時候直接暈菜。這篇文章我就會講解一些語法上的東西。
語法
Gradle指令碼是使用Groovy語言來寫的。Groovy的語法有點像Java,希望你能接受它。
如果你對Groovy已經很熟悉了,可以跳過這部分了。
Groovy中有一個很重要的概念你必要要弄懂–Closure(閉包)
Closures
Closure是我們弄懂Gradle的關鍵。Closure是一段單獨的程式碼塊,它可以接收引數,返回值,也可以被賦值給變數。和Java中的Callable介面,Future類似,也像函式指標,你自己怎麼方便理解都好。。。
關鍵是這塊程式碼會在你呼叫的時候執行,而不是在建立的時候。看一個Closure的例子:
1 2 3 4 5 6 |
def myClosure = { println 'Hello world!' } //execute our closure myClosure() #output: Hello world! |
下面是一個接收引數的Closure:
1 2 3 4 5 6 |
def myClosure = {String str -> println str } //execute our closure myClosure('Hello world!') #output: Hello world! |
如果Closure只接收一個引數,可以使用it來引用這個引數:
1 2 3 4 5 6 |
def myClosure = {println it } //execute our closure myClosure('Hello world!') #output: Hello world! |
接收多個引數的Closure:
1 2 3 4 5 6 |
def myClosure = {String str, int num -> println "$str : $num" } //execute our closure myClosure('my string', 21) #output: my string : 21 |
另外,引數的型別是可選的,上面的例子可以簡寫成這樣:
1 2 3 4 5 6 |
def myClosure = {str, num -> println "$str : $num" } //execute our closure myClosure('my string', 21) #output: my string : 21 |
很酷的是Closure中可以使用當前上下文中的變數。預設情況下,當前的上下文就是closure被建立時所在的類:
1 2 3 4 5 |
def myVar = 'Hello World!' def myClosure = {println myVar} myClosure() #output: Hello world! |
另外一個很酷的點是closure的上下文是可以改變的,通過Closure#setDelegate()。這個特性非常有用:
1 2 3 4 5 6 7 8 9 10 |
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! |
正如你鎖看見的,在建立closure的時候,myVar並不存在。這並沒有什麼問題,因為當我們執行closure的時候,在closure的上下文中,myVar是存在的。這個例子中。因為我在執行closure之前改變了它的上下文為m,因此myVar是存在的。
把closure當做引數傳遞
closure的好處就是可以傳遞給不同的方法,這樣可以幫助我們解耦執行邏輯。前面的例子中我已經展示瞭如何把closure傳遞給一個類的例項。下面我們將看一下各種接收closure作為引數的方法:
1.只接收一個引數,且引數是closure的方法: myMethod(myClosure)
2.如果方法只接收一個引數,括號可以省略: myMethod myClosure
3.可以使用內聯的closure: myMethod {println ‘Hello World’}
4.接收兩個引數的方法: myMethod(arg1, myClosure)
5.和4類似,單數closure是內聯的: myMethod(arg1, { println ‘Hello World’ })
6.如果最後一個引數是closure,它可以從小括號從拿出來: myMethod(arg1) { println ‘Hello World’ }
這裡我只想提醒你一下,3和6的寫法是不是看起來很眼熟?
Gradle
現在我們已經瞭解了基本的語法了,那麼如何在Gradle指令碼中使用呢?先看下面的例子吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:1.2.3' } } allprojects { repositories { jcenter() } } |
知道了Groovy的語法,是不是上面的例子就很好理解了?
首先就是一個buildscript方法,它接收一個closure:
1 |
def buildscript(Closure closure) |
接著是allprojects方法,它也接收一個closure引數:
1 |
def allprojects(Closure closure) |
其他的都類似。。。
現在看起來容易多了,但是還有一點不明白,那就是這些方法是在哪裡定義的?答案就是Project
Project
這是理解Gradle指令碼的一個關鍵。
構建指令碼頂層的語句塊都會被委託給Project的例項
這就說明Project正是我要找得地方。
在Project的文件頁面搜尋buildscript方法,會找到buildscript{} script block(指令碼塊).等等,script block是什麼鬼?根據文件:
script block就是隻接收closure作為引數的方法
繼續閱讀buildscript的文件,文件上說Delegates to: ScriptHandler from buildscript。也就是說,我們傳遞給buildscript方法的closure,最終執行的上下文是ScriptHandler。在上面的例子中,我們的傳遞給buildscript的closure呼叫了repositories(closure)和dependencies(closure)方法。既然closure被委託給了ScriptHandler,那麼我們就去ScriptHandler中尋找dependencies方法。
找到了void dependencies(Closure configureClosure),根據文件,dependencies是用來配置指令碼的依賴的。而dependencies最終又是委託到了DependencyHandler。
看到了Gradles是多麼廣泛的使用委託了吧。理解委託是很重要滴。
Script blocks
預設情況下,Project中預先定義了很多script block,但是Gradle外掛允許我們自己定義新的script blocks!
這就意味著,如果你在build指令碼頂層發了一些{…},但是你在Gradle的文件中卻找不到這個script blocks或者方法,絕大多情況下,這是一些來自外掛中定義的script block。
android Script block
我們來看看預設的Android app/build.gradle檔案:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
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方法,它接收一個closure引數。但是如果我們在Project的文件中搜尋,是找不到這個方法的。原因很簡單,這不是在Project中定義的方法。
仔細檢視build指令碼,可以看到在抵用android方法之前,我們使用了com.android.application外掛。Android application外掛擴充套件了Project物件,新增了android Script block。
哪裡可以找到Android外掛的文件呢?Android Tools website或者這裡
如果我們開啟AppExtension的文件,我們就可以找打build script中使用的方法了屬性:
1.compileSdkVersion 22. 如果我們搜尋compileSdkVersion,將會找到這個屬性. 這裡我們給這個屬性賦值 “22”。
2.buildToolsVersion和1類似
3.defaultConfig – 是一個script block將會委託給ProductFlavor類來執行。
4.其他
現在我們已經能夠理解Gradle指令碼的語法了,也知道如何查詢文件了。
練習
來配置點東西練練手吧。
在AppExtension中,我找了一個script block testOptions,它會把closure引數委託給TestOptions類。根據文件,TestOptions類有兩個屬性:reportDir和resultsDir。reportDir是用來儲存test報告的。我們來改改這個屬性試試。
1 2 3 4 5 6 |
android { ...... testOptions { reportDir "$rootDir/test_reports" } } |
這裡我使用了Project中定義的rootDir屬性,該屬性值為專案的根目錄。
現在如果我執行./gradlew connectedCheck,test報告將會被儲存到[rootProject]/test_reports目錄中。
不要在真實的專案嘗試這個修改,所有的構建輸出都應該在build目錄中,這樣才不至於汙染你的專案目錄結構。