Groovy&Gradle總結

歡迎你們加入QQ羣一塊兒討論: 489873144(android格調小窩)
個人github地址:https://github.com/jeasonlzyjavascript

0x01 Groovy 概述

Groovy 是一個基於 JVM 的語言,代碼最終編譯成字節碼(bytecode),並在 JVM 上運行。它具備相似於 Java 的語法風格,可是語法又比 Java 要靈活和方便,同時具備動態語言(如 ruby 和 Python)的一些特性。html

正由於如此,因此Groovy適合用來定義DSL(Domain Specific Language)。java

簡單的來說 DSL 是一個面向特定小領域的語言,如常見的 HTML、CSS 都是 DSL,它一般是以配置的方式進行編程,與之相對的是通用語言(General Purpose Language),如 Java 等。python

0x02 groovy 基本知識

1)首先須要安裝groovy環境,具體的環境安裝就不說了,網上不少,安裝完成後配置環境變量,出現如下結果,即安裝成功

2)groovy與java

由於Groovy是基於JVM的語言,因此咱們來看看最後生成的字節碼文件。咱們寫一個類:
hello.groovyandroid

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
name = "lzy" def say(){ "my name is $name" } println say()

在命令行輸入groovy hello.groovy,運行腳本,輸出如下結果:

上面的操做作完後,有什麼感受和體會?
最大的感受可能就是groovy和shell腳本,或者python好相似。
另外,除了能夠直接使用JDK以外,Groovy還有本身的一套GDK。 c++

咱們看一下編譯成jvm字節碼後的結果git

咱們輸入groovyc -d classes hello.groovy命令將當前文件生成字節碼文件,-d參數表示在classes文件夾下,最終結果以下:
hello.classgithub

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
import groovy.lang.Binding; import groovy.lang.Script; import org.codehaus.groovy.runtime.BytecodeInterface8; import org.codehaus.groovy.runtime.GStringImpl; import org.codehaus.groovy.runtime.InvokerHelper; import org.codehaus.groovy.runtime.ScriptBytecodeAdapter; import org.codehaus.groovy.runtime.callsite.CallSite; public class hello extends Script { public hello() { CallSite[] var1 = $getCallSiteArray(); } public hello(Binding context) { CallSite[] var2 = $getCallSiteArray(); super(context); } public static void main(String... args) { CallSite[] var1 = $getCallSiteArray(); var1[0].call(InvokerHelper.class, hello.class, args); } public Object run() { CallSite[] var1 = $getCallSiteArray(); String var2 = "lzy"; ScriptBytecodeAdapter.setGroovyObjectProperty(var2, hello.class, this, (String)"name"); return !__$stMC && !BytecodeInterface8.disabledStandardMetaClass()?var1[3].callCurrent(this, this.say()):var1[1].callCurrent(this, var1[2].callCurrent(this)); } public Object say() { CallSite[] var1 = $getCallSiteArray(); return new GStringImpl(new Object[]{var1[4].callGroovyObjectGetProperty(this)}, new String[]{"my name is ", ""}); } }

到這裏咱們能夠發現,其實groovy腳本本質就是java,他與java幾乎沒有區別,只是在java語言在語法上的擴展,支持DSL,加強了可讀性。而且咱們得出如下結論:
1. hello.groovy被轉換成了一個hello類,它從Script派生。
2. 每個腳本都會生成一個static main函數。這樣,當咱們groovy hello.groovy去執行的時候,其實就是用java去執行了這個main函數
3. 腳本中的全部代碼都會放到run函數中。好比,say()方法的調用,這句代碼其實是包含在run()方法裏的。
4. 若是腳本中定義了方法,則方法會被定義在hello類中。
5. 腳本中定義的變量是有它的做用域的,name = 「lzy」,這句話是在run()中建立的。因此,name看起來好像是在整個腳本中定義的,但實際
上say()方法沒法直接訪問它。web

接着咱們把上述的hello.groovy文件修改,在定義name前的加上def修飾符,其他不作任何修改,咱們再次運行代碼,發現如下錯誤:
shell

咱們將修改後的代碼編譯成class文件後,與以前的正常結果作對比,發現如下不一樣:

左邊是正確的,右邊是錯誤的,相比下來就是多調用了一個方法,這個方法看起來就是將你定義的屬性保存到了某個全局的環境中,確保下面的say()方法在調用的時候,能從全局取到這個屬性。

  
  
  
  
  • 1
ScriptBytecodeAdapter.setGroovyObjectProperty(var2, hello.class, this, (String)"name");

可是這樣仍是與咱們的想象有差距,name並無在成員位置,那如何才能才能讓咱們定義的屬性就生成在成員變量的位置呢?這時候須要@Field註解,以下:

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
import groovy.transform.Field @Field name = "lzy" def say(){ "my name is $name" } println say()

加上這行註解後,生成的字節碼以下:
name屬性確實變成了成員變量,而且是在構造方法中被初始化了。

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
import groovy.lang.Binding; import groovy.lang.Script; import org.codehaus.groovy.runtime.BytecodeInterface8; import org.codehaus.groovy.runtime.GStringImpl; import org.codehaus.groovy.runtime.InvokerHelper; import org.codehaus.groovy.runtime.callsite.CallSite; public class hello extends Script { Object name; public hello() { CallSite[] var1 = $getCallSiteArray(); String var2 = "lzy"; this.name = var2; } public hello(Binding context) { CallSite[] var2 = $getCallSiteArray(); super(context); String var3 = "lzy"; this.name = var3; } public static void main(String... args) { CallSite[] var1 = $getCallSiteArray(); var1[0].call(InvokerHelper.class, hello.class, args); } public Object run() { CallSite[] var1 = $getCallSiteArray(); Object var10000 = null; return !__$stMC && !BytecodeInterface8.disabledStandardMetaClass()?var1[3].callCurrent(this, this.say()):var1[1].callCurrent(this, var1[2].callCurrent(this)); } public Object say() { CallSite[] var1 = $getCallSiteArray(); return new GStringImpl(new Object[]{this.name}, new String[]{"my name is ", ""}); } }

0x03 Groovy 語法

這裏只講一些比較重要的特性,其他比較基本的語法比較簡單,能夠參考這裏過一遍:
工匠若水的博客:Groovy腳本基礎全攻略

1)方法的輸入參數優化

groovy中定義的函數,若是至少有一個參數,在調用的時候能夠省略括號。若是某個函數沒有參數,那就不能省略括號,不然會當成一個變量使用。

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
def func(String a){ println(a) } func 'hello'

在android項目中,好比build.gradle

  
  
  
  
  • 1
  • 2
  • 3
  • 4
android { compileSdkVersion 25 buildToolsVersion "25.0.0" }

好比這裏compileSdkVersion 和 buildToolsVersion 其實就是調用了一樣名字的兩個函數,在AndroidStudio裏面能夠點進去查看函數實現

2)閉包

閉包的概念也許咱們稍微陌生一點,可是實際上,咱們能夠簡單把它當作一個匿名類,只是編譯器提供了更加簡單的語法來實現它的功能。
閉包(Closure)是groovy中一個很重要的概念,並且在gradle中普遍使用。簡而言之,閉包就是一個可執行的代碼塊,相似於C語言中的函數指針。在不少動態類型語言中都有普遍的使用,java8 中也有相似的概念:lambda expression,可是groovy中的閉包和java8中的lambda表達式相比又有不少的不一樣之處。

咱們能夠把閉包當作一個匿名內部類,只是編譯器提供了更加簡單的語法來實現它的功能。在Groovy中閉包也是對象,能夠像方法同樣傳遞參數,而且能夠在須要的地方執行。

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
def clos = { params -> println "Hello ${params}" } clos("World") //Closure類型的實例,好比上面的閉包咱們又能夠定義爲: //參數能夠聲明類型,也能夠不聲明,還能夠有缺省值 Closure clos1 = { a, def b, int c = 2 -> a + b + c //默認返回值就是最後一行計算的結果,return關鍵字可省略 } println clos1(5, 3) //能夠制定一個可選的返回類型 //若是閉包內沒有生命任何參數,沒有->, 那麼閉包內置會定義一個隱含參數it Closure<String> clos2 = { println it return "clos2" }

閉包有三個很重要的屬性分別是:this,owner,delegate,分別表明如下概念:

  • this: 對應於定義閉包時包含他的class,能夠經過getThisObject或者直接this獲取
  • owner: 對應於定義閉包時包含他的對象,能夠經過getOwner或者直接owner獲取
  • delegate: 閉包對象能夠指定一個第三方對象做爲其代理,用於函數調用或者屬性的指定,能夠經過getDelgate或者delegate屬性獲取

咱們編寫以下代碼:test1.groovy

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
class A { def closure1 = { println "--------------closure1--------------" println "this:" + this.class.name println "owner:" + owner.class.name println "delegate:" + delegate.class.name def closure2 = { println "-------------closure2---------------" println "this:" + this.class.name println "owner:" + owner.class.name println "delegate:" + delegate.class.name def closure3 = { println "-------------closure3---------------" println "this:" + this.class.name println "owner:" + owner.class.name println "delegate:" + delegate.class.name } closure3() } closure2() } } def a = new A() def closure1 = a.closure1 closure1()

運行後獲得以下結果:

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
--------------closure1-------------- this:com.lzy.A owner:com.lzy.A delegate:com.lzy.A -------------closure2--------------- this:com.lzy.A owner:com.lzy.A$_closure1 delegate:com.lzy.A$_closure1 -------------closure3--------------- this:com.lzy.A owner:com.lzy.A$_closure1$_closure2 delegate:com.lzy.A$_closure1$_closure2

3)代理策略

若是在閉包內,沒有明確指定屬性或者方法的調用是發生在this, owner,delegate上時,就須要根據代理策略來判斷到底該發生在誰身上。有以下幾種代理策略:

  • Closure.OWNER_FIRST 默認的策略,若是屬性或者方法在owner中存在,調用就發生在owner身上,不然發生在delegate上
  • Closure.DELEGATE_FIRST 跟owner_first正好相反
  • Closure.OWNER_ONLY 忽略delegate
  • Closure.DELEGATE_ONLY 忽略owner
  • Closure.TO_SELF 調用不發生在owner或delegate上,只發生在閉包內

咱們編寫以下代碼:test2.groovy

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
import groovy.transform.Field @Field String name = "abc" class P { String name def pretty = { "my name is $name" } } class T { String name } def upper = { name.toUpperCase() } println upper() def p = new P(name: 'ppp') def t = new T(name: 'ttt') upper.delegate = t upper.resolveStrategy = Closure.DELEGATE_FIRST println upper() p.pretty.delegate = this println p.pretty() p.pretty.resolveStrategy = Closure.DELEGATE_FIRST println p.pretty()

運行後獲得以下結果:

  
  
  
  
  • 1
  • 2
  • 3
  • 4
ABC TTT my name is ppp my name is abc

這裏重點強調一下,成員變量name必定要加上@Field註解,否者出現的結果必定不是上述結果,緣由以前已經分析過,不加這個註解,name屬性將不會是成員變量

4)類的Property

Groovy中的class和java中的Class區別不大,值得咱們關注的區別是,若是類的成員變量沒有加任何權限訪問,則稱爲Property, 不然是Field,filed和Java中的成員變量相同,可是Property的話,它是一個private field和getter setter的集合,也就是說groovy會自動生成getter setter方法,所以在類外面的代碼,都是會透明的調用getter和setter方法

咱們在上述的test1.groovy的類A中加入如下幾行代碼:

  
  
  
  
  • 1
  • 2
  • 3
String name = "aaa" public String name1 = "bbb" private String name2 = "ccc"

而後對這個test1.groovy進行編譯groovyc -d classes test1.groovy,生成的文件結構以下:

點開A.class發現如下代碼:

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
public class A implements GroovyObject { private String name; public String name1; private String name2; private Object closure1; ··· public String getName() { return this.name; } public void setName(String var1) { this.name = var1; } ··· }

5)操做符重載

咱們常常在gradle中看見如下代碼:

  
  
  
  
  • 1
  • 2
  • 3
task hello << { println "hello world" }

實際上他的原始調用含義是:

  
  
  
  
  • 1
task("hello").leftShift(closure)

由於task重載了leftShift,因此可使用 << 操做符,這和c++的特性是同樣的

6)Command Chains

這個特性不只能夠省略函數調用中的括號,並且能夠省略,連續函數調用中的. 點號, 好比
a(b).c(d) 這裏a c是函數, b d是函數參數, 就能夠縮寫爲a b c d。這個特性強大之處在於不只適用於單個參數類型函數,並且適用於多個參數類型的函數,當參數類型爲閉包時一樣適用。

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
task("task1").doLast({ println "111" }).doLast({ println("222") }) //簡寫 task task1 doLast { println "111" } doLast { println("222") }

7)DSL

藉助閉包的特性,咱們能夠嘗試寫一個簡單的DSL。下面的代碼展現瞭如何藉助groovy的語法特性來實現一個DSL,這些特性咱們稍後會在gradle的腳本中看到。

test3.groovy

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
class Book { def _name = '' def _price = 0.0 def shop = [] def static config(config){ Book book = new Book(shop:['A','B']) config.delegate = book config() } def name(name){ this._name = name } def price(price){ this._price = price } def getDetail(){ println "name : ${_name}" println "price : ${_price}" println "shop : ${shop}" } } Book.config { name 'test' price 1.2 detail }

上面所提到的這些groovy的語法特性,構成了Gradle中DSL的基礎

0x04 Gradle 基本概念

咱們在AndroidStudio中建立基於Gradle的project時,會默認生成一個多項目結構的Gradle工程,他有以下結構:

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
├── app │ └── build.gradle ├── lib │ └── build.gradle ├── build.gradle └── settings.gradle

若是是單工程結構,這個Setting.gradle其實能夠省略

Gradle中,每個待編譯的工程,或者叫每個Library和App都是單獨的Project。根據Gradle的要求,每個Project在其根目錄下都須要有一個build.gradle。build.gradle文件就是該Project的編譯腳本,相似於Makefile。每個Project在構建的時候都包含一系列的Task。好比一個Android APK的編譯可能包含:Java源碼編譯Task、資源編譯Task、JNI編譯Task、lint檢查Task、打包生成APK的Task、簽名Task等。具體一個Project到底包含多少個Task,實際上是由編譯腳本指定的插件決定。插件是什麼呢?插件就是用來定義Task,並具體執行這些Task的東西。

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
apply plugin: 'com.android.application' //app插件 apply plugin: 'com.android.library' //lib插件 android{ ... } dependencies{ .... }

若是咱們把以上代碼在build.gradle中刪掉,執行gradle tasks命令列出全部可執行的task,會發現不少task都不見了,由此咱們能得出結論,

這些task都是android application插件生成的。咱們能使用Gradle構建Android 工程,一切都基於這個插件。這個插件從android這個擴展中讀取了咱們的配置,生成了一些列構建android 所須要的任務。

咱們執行gradle projects列出全部的工程:

這個圖片的最後幾句話也告訴了咱們,若是你在根目錄下,想查看或者運行某個項目下的任務,能夠用gradle :app:tasks這種語法,若是你cd到子項目的根目錄下,是不須要加:app這樣的前綴的。

對Android來講,gradle assemble這個Task會生成最終的產物Apk,因此若是一個工程包含5個Model,那麼須要分別編譯這5個,他們可能還有一些依賴關係,這樣就很麻煩了,而在Gradle中,是支持多工程編譯的(Multi-Projects Build),咱們在根目錄下直接執行gradle assemble,就能按照依賴關係把這5個Model所有編譯出來生成最終的Apk,可是爲何能夠呢?

  1. 咱們須要在根目錄下也添加一個build.gradle。這個build.gradle通常幹得活是:配置其餘子Project的。好比爲子Project添加一些屬性。這個build.gradle有沒有都無所謂。

  2. 繼續在根目錄下添加一個名爲settings.gradle。這個文件很重要,名字必須是settings.gradle。它裏邊用來告訴Gradle,這個multi-projects包含多少個子Project,內部通常就是一個include指令。根據groovy的語法,他就是在gradle生成的settings對象調用函數 include(‘app’),include接受的參數是一個string數組,所以include後能夠加不少參數,這個函數的意義就是:指明那些子project參與此次gradle構建

因此對於一個工程,咱們能對構建過程作出改變的,就只能發生在這些.gradle文件中,這些文件稱爲Build Script構建腳本。對於Gradle中的構建腳本,一方面能夠理解爲配置文件,每一種類型腳本文件都是對某一種類型的構建對象進行配置。另外一方面也能夠把每一個腳本理解爲一個Groovy閉包,這樣咱們在執行構建腳本時,就是在執行每個閉包函數,只不過每一個閉包所設置的delegate不同。

如下來自於文檔:Gradle Build Language Reference這個文檔很重要,後面會常用!!!

  • Project對象:每一個build.gradle會轉換成一個Project對象,或者說代理對象就是Project。
  • Gradle對象:當咱們執行gradle xxx或者什麼的時候,gradle會從默認的配置腳本中構造出一個Gradle對象。在整個執行過程當中,只有這麼一個對象。Gradle對象的數據類型就是Gradle。咱們通常不多去定製這個默認的配置腳本。
  • Settings對象:每一個settings.gradle會轉換成一個Settings對象,或者說代理對象是Setting。

補充一點:Init Script其實就是配置gradle運行環境。彷佛曆來沒有使用過,可是在每一次構建開始以前,都會執行init script,咱們能夠對當前的build作出一些全局配置,好比全局依賴,何處尋找插件等。有多個位置能夠存放init script以下:
1. 經過在命令行裏指定gradle參數 -I 或者–init-script

1)Build生命週期

Gradle的構建腳本生命週期具有三大步,以下:

上圖告訴咱們如下信息,

  1. Gradle工做包含三個階段:
  2. 首先是初始化階段。對咱們前面的multi-project build而言,就是執行settings.gradle
  3. Initiliazation phase的下一個階段是Configration階段。
  4. Configration階段的目標是解析每一個project中的build.gradle。好比multi-project build例子中,
    解析每一個子目錄中的build.gradle。在這兩個階段之間,咱們能夠加一些定製化的Hook。這固然是經過
    API來添加的,須要特別注意:每一個Project都會被解析。
  5. Configuration階段完了後,整個build的project以及內部的Task關係就肯定了。一個
    Project包含不少Task,每一個Task之間有依賴關係。Configuration會創建一個有向圖來描述Task之間的
    依賴關係,是一個有向圖,因此,咱們能夠添加一個HOOK,即當Task關係圖創建好後,執行一些操做.
  6. 最後一個階段就是執行任務了。你在gradle xxx中指定什麼任務,gradle就會將這個xxx任務鏈上的全部任務所有按依賴順序執行一遍!固然,任務執行完後,咱們還能夠加Hook。

gradle具備如下幾個經常使用的命令:
gradle tasks //列出全部的任務
gradle projects //列出全部的項目
gradle properties //列出全部的屬性

2)Setting對象

先看文檔,方法文檔都在文檔中:Settings
其中有這麼句話比較重要:

In addition to the properties of this interface, the Settings object makes some additional read-only properties available to the settings script. This includes properties from the following sources:

  • Defined in the gradle.properties file located in the settings directory of the build.
  • Defined the gradle.properties file located in the user’s .gradle directory.
  • Provided on the command-line using the -P option.

翻譯後就是,除了Setting這個接口本身提供的屬性方法外,你還能夠在如下位置添加本身的額外屬性:
- setting.gradle 平級目錄下的 gradle.properties 文件
- 用戶.gradle目錄下的 gradle.properties 文件
- 使用命令行 -P 屬性

其他的文檔中比較詳細。

3)Project對象

如下內容均來自與文檔:Project

每個build.gradle文件和一個Project對象一一對應,在執行構建的時候,gradle經過如下方式爲每個工程建立一個Project對象:

  1. 建立一個Settings對象,
  2. 根據settings.gradle文件配置它
  3. 根據Settings對象中定義的工程的父子關係建立Project對象
  4. 執行每個工程的build.gradle文件配置上一步中建立的Project對像

其中有不少有用的方法:

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
//apply一個插件或者腳本 void apply(Map<String, ?> options); //配置當前project的依賴 void dependencies(Closure configureClosure); //配置當前腳本的classpath void buildscript(Closure configureClosure);

在build.gradle文件中定義的屬性和方法會委託給Project對象執行,每個project對象在尋找一個屬性的時候有5個做用域做爲範圍,分別是:

屬性可見範圍

  • 1.Project 自己
  • 2.Project的ext屬性
  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
project.ext.prop1 = "prop1" ext.prop2 = "prop2" project.ext { prop3 = "prop3" prop4 = "prop4" } ext { prop5 = "prop5" prop6 = "prop6" } println project.ext.prop1 println project.ext.prop2 println project.prop3 println project.prop4 println prop5 println prop6
  • 3.經過plugin添加的extension,就是插件定義本身的特有擴展屬性,每個extension經過一個和extension同名的只讀屬性訪問
  • 4.經過plugin添加的屬性。一個plugin能夠經過Project的Convention對象爲project添加屬性和方法。
  • 5.project中的task,一個task對象能夠經過project中的同名屬性訪問,可是它是隻讀的
  • 6.當前project的父工程的extra屬性和convention屬性,是隻讀的

當獲取或者設置這些屬性的時候,按照上述的順序依次尋找,若是都沒找到,則拋出異常。

方法可見範圍

  1. Project對象自己
  2. build.gradle文件中定義的方法
  3. 經過plugin添加的extension,每一個extensions均可以做爲一個方法訪問,它接受一個閉包或Action做爲參數
  4. 經過plugin添加的方法。一個plugin能夠經過Project的Convention對象爲project添加屬性和方法。
  5. project中的task,每個task 都會在當前project中存在一個接受一個閉包或者Action做爲參數的方法,這個閉包會在task的configure(closure)方法中調用。
  6. 當前工程的父工程中的方法
  7. 當前工程的屬性可見範圍中全部的閉包屬性均可以做爲方法訪問

4) Task對象

先來文檔 Task

A Task is made up of a sequence of Action objects. When the task is executed, each of the actions is executed in turn, by calling Action.execute(T). You can add actions to a task by calling Task.doFirst(org.gradle.api.Action) or Task.doLast(org.gradle.api.Action).

Groovy closures can also be used to provide a task action. When the action is executed, the closure is called with the task as parameter. You can add action closures to a task by calling Task.doFirst(groovy.lang.Closure) or Task.doLast(groovy.lang.Closure).

There are 2 special exceptions which a task action can throw to abort execution and continue without failing the build. A task action can abort execution of the action and continue to the next action of the task by throwing a StopActionException. A task action can abort execution of the task and continue to the next task by throwing a StopExecutionException. Using these exceptions allows you to have precondition actions which skip execution of the task, or part of the task, if not true.

建立一個簡單的task的語法爲:

  
  
  
  
  • 1
  • 2
task <taskName> << { }

這句話應該這麼理解:

  1. 首先調用project的task方法,傳入一個taskName,返回一個task
  2. 調用task的leftShift 方法 傳入一個closure,根據leftShift的解釋,咱們知道這個閉包將添加到task的action list裏去,在任務執行的時候運行

有時候,咱們可能會錯寫成

  
  
  
  
  • 1
  • 2
task <taskName> { }

少了這個<< 操做符,意思就徹底不同了,這個時候調用的函數爲

  
  
  
  
  • 1
  • 2
//Project類 Task task(String name, Closure configureClosure);

這時,第二個參數closure用來配置task,在task建立的時候,也就是構建整個任務有向圖的時候執行,而不是在task執行的時候運行。不過咱們能夠在這個閉包內配置task的一些屬性。

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
//指定Copy類型task的屬性 task copyDocs(type: Copy) { from 'src/main/doc' into 'build/target/doc' }

固然咱們還能夠這樣指定task的行爲:

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
task exampleTask { doLast{ } } task exampleTask doLast{ } task exampleTask << { }

還能夠指定task的類型

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
task name(type: Type){ doLast{ } }

gradle內置爲咱們生成了不少task類型,好比Copy,Delete,能夠點擊連接 查看gradle內置的task類型列表,若是建立task時沒有指定type,則他默認是DefaultTask類型。咱們還能夠建立本身的task類型,咱們在稍後就會講到。

咱們還能夠能夠指定task之間的依賴關係, 經過dependsOn, mustRunAfter, shouldRunAfter來指定。 還能夠指定task的分組group, 若是不指定,將會出如今other裏面。

5) 構建的生命週期測試

如下各個方法參考文檔:
Gradle相關
Task相關

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
├── app │ └── build.gradle ├── lib │ └── build.gradle ├── build.gradle └── settings.gradle

root/settings.gradle

  
  
  
  
  • 1
  • 2
  • 3
println '------setting.gradle execute------' include ':app', ':lib', ':helloPlugin', ':simplePlugin'

root/build.gradle

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
println '------root build.gradle execute------' apply from: uri('./build_1.gradle') apply from: uri('./build_2.gradle') println "all project size : ${allprojects.size()}" gradle.settingsEvaluated { settings -> println "settingsEvaluated" } gradle.projectsLoaded { gradle -> println "projectsLoaded" } gradle.beforeProject { project -> println "beforeProject: ${project.name} " } gradle.afterProject { project -> println "afterProject: ${project.name}" } gradle.projectsEvaluated { gradle -> println "projectsEvaluated" } gradle.buildFinished { buildResult -> println "buildFinished" } gradle.taskGraph.whenReady { graph -> println "============task graph is ready============" graph.getAllTasks().each { println "task ${it.name} will execute" } println "============task graph is over=============" } gradle.taskGraph.beforeTask { task -> println "before ${task.name} execute" } gradle.taskGraph.afterTask { task -> println "after ${task.name} execute" } tasks.whenTaskAdded { task -> println "taskAdded:" + task.name } task subTask1 { group "hello" doLast { println "${name} execute" } } task subTask2(dependsOn: 'subTask1') { group "hello" doLast { println "${name} execute" } }

app/build.gradle

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
println '------app build.gradle execute------' beforeEvaluate { project -> println "beforeEvaluate: ${project.name} --in--" } afterEvaluate { project -> println "beforeEvaluate: ${project.name} --in--" }

lib/build.gradle

  
  
  
  
  • 1
println '------lib build.gradle execute------'

在根目錄下執行 gradle libTask2

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
------setting.gradle execute------ ------root build.gradle execute------ all project size : 5 taskAdded:subTask1 taskAdded:subTask2 afterProject: GradlePlugin afterEvaluate: GradlePlugin beforeProject: app beforeEvaluate: app ------app build.gradle execute------ afterProject: app afterEvaluate: app Incremental java compilation is an incubating feature. beforeEvaluate: app --in-- beforeProject: lib beforeEvaluate: lib ------lib build.gradle execute------ afterProject: lib afterEvaluate: lib projectsEvaluated ============task graph is ready============ task subTask1 will execute task subTask2 will execute ============task graph is over============= :subTask1 before subTask1 execute subTask1 execute after subTask1 execute :subTask2 before subTask2 execute subTask2 execute after subTask2 execute

從上述例子中咱們驗證瞭如下結果:

  1. 若是一個task在build.gradle中定義,可是在構建中不會執行,那麼它的Task對象會建立,可是不會在任務圖中出現。
  2. 咱們能夠經過Gradle或者Project對象中定義的方法獲取生命週期中每個過程在執行中的回調。這裏注意一下,咱們定義的一些回調在實際執行中彷佛並無被觸發,例如,settingsEvaluated,projectsLoaded。具體緣由須要細看。

0x05 自定義一個插件

首先看一下工程結構

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
├── app //root工程 ├── repo //本地maven目錄 ├── helloPlugin //plugin工程 │ ├── build.gradle │ └── src │ └── main │ ├── groovy │ │ └── com.lzy.plugin │ │ ├── HelloPlugin.groovy │ │ ├── Person.groovy │ │ └── PersonExt.groovy │ └── resources │ └── META-INF │ └── gradle-plugins │ └── helloPlugin.properties //插件名 │ ├── build.gradle └── settings.gradle

首先,插件工程能夠用任意的jvm語言編寫,例如,scala,groovy,java等,最終每個插件都會打包成一個jar包,其中META-INF文件下中每個.properties文件表明一個Plugin,最後使用的時候以下:

  
  
  
  
  • 1
apply plugin: 'helloPlugin'

這個文件裏面內容指明瞭插件類的全類名,以下:

  
  
  
  
  • 1
implementation-class=com.lzy.plugin.HelloPlugin

HelloPlugin.groovy:很簡單,就定義一個任務,打印一個字符串

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
package com.lzy.plugin import org.gradle.api.Plugin import org.gradle.api.Project class HelloPlugin implements Plugin<Project> { public void apply(Project project) { project.task("sayHello") { group "hello" doLast { println "Hello Plugin" } } } }

首先咱們必須說明的是,插件能夠以三種形式存在:
1. 在咱們構建項目的build.gradle腳本中直接編寫
2. 在咱們構建項目的rootProjectDir/buildSrc/src/main/groovy 目錄下
3. 以單獨的project存在

這裏採用第三種方式:在插件目錄下編寫

這種編寫方式只能發佈到本地
build.gradle

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
apply plugin: 'groovy' apply plugin: 'maven' version = '0.1.1' group = 'com.lzy.plugin' repositories { mavenCentral() } dependencies { compile gradleApi() compile localGroovy() } uploadArchives { repositories.mavenDeployer { repository(url: 'file:../repo') } }

有時咱們更想開源出去給其餘人用,像Small這樣,咱們就能夠這麼寫
build.gradle

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
apply plugin: 'maven-publish' publishing { publications { mavenJava(MavenPublication) { from components.java //這兩行是發佈源碼和文檔的,能夠不發佈 artifact sourcesJar artifact javadocJar groupId 'com.lzy.plugin' artifactId 'helloPlugin' version '0.1.1' } } repositories { maven { url "../repo" } } } //默認打包時只會包含編譯過的jar包,咱們能夠增長如下兩個task,將源代碼和javadoc打包發佈,並經過上述artifact指定: task javadocJar(type: Jar, dependsOn: groovydoc) { classifier = 'javadoc' from "${buildDir}/javadoc" } task sourcesJar(type: Jar) { from sourceSets.main.allSource classifier = 'sources' }

groupId、artifactId、version,這三者組成了插件使用者在聲明依賴時的完整語句 groupId:artifactId:version

對於第一種方式:在helloPlugin的根目錄下執行,gralde uploadArchives,編譯插件工程,併發布到../repo目錄。

對於第二種方式:有兩種publish任務,publish 和 publishToMavenLocal,
- publish:任務依賴於全部的mavenPublication的generatePomFileFor任務和publishxxxPublicationToMavenRepository,意思是將全部的mavenPublication發佈到指定的repository,
- publishToMavenLocal依賴於全部的mavenPublication的generatePomFileFor和publishxxxTomavenLocal任務,意思是將全部的mavenPublication發佈到本地的m2 repository。

以上,咱們就建立好了一個gradle plugin,那麼如何使用它呢?

首先,在root工程下的build.gradle中,咱們經過buildscript引入插件

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
buildscript{ repositories{ mavenCentral() maven { url uri('./repo') } } dependencies { classpath 'com.lzy.plugin:helloPlugin:0.1.1' } }

而後,在app工程下,咱們應用這個插件,

  
  
  
  
  • 1
apply plugin: 'helloPlugin'

最後,在app或者根目錄下執行,gradle sayHello,這樣就打印出了咱們插件中定義的文字。

以上就是一個自定義插件的建立和應用過程,雖然很簡單,可是能夠幫助咱們理解gradle是如何經過plugin完成不少複雜的工做的。

Extension

1)狀況一:

有時候咱們但願在使用插件的時候,額外配置一些參數,這時候就須要額外寫一個Ext類,以下:
PersonExt.groovy

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
public class PersonExt { String name int age boolean boy @Override public String toString() { return "I am $name, $age years old, " + (boy ? "I am a boy" : "I am a girl") } }

接着修改
HelloPlugin.groovy

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
class HelloPlugin implements Plugin<Project> { public void apply(Project project) { project.extensions.add("person", PersonExt) project.task("sayHello") { group "hello" doLast { //如下兩種方式均可以 def personExt = project.person def personExt1 = project.extensions.getByName('person') println personExt println personExt1 } } } }

最後咱們再使用插件的地方添加如下屬性:

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
person { name = "abc" age = 18 boy = true }

這裏使用=號進行復制,實際上可加可不加,本質是利用了grooy特性,調用了setName方法,而且省略了方法調用的括號。

執行 gradle sayHello獲得以下結果:

  
  
  
  
  • 1
  • 2
  • 3
:sayHello I am abc, 18 years old, I am a boy I am abc, 18 years old, I am a boy

到這裏說明咱們定義的Extension正確設置並讀取成功了。

2)狀況二

若是咱們但願設置的Extension是一個集合列表,而且該列表長度未知,又該怎麼寫呢?

咱們須要使用NamedDomainObjectContainer,咱們後面都簡稱NDOC 這是一個容納object的容器,它的特色是它的內部使用SortedSet實現的,內部對象的name是unique的,並且是按name進行排序的。一般建立NDOC的方法就是調用Project裏的方法:

這裏type有一個要求:必須有一個public的構造函數,接受string做爲一個參數,必須有一個叫作name 的property。

新增一個類:
HobbyExt.groovy

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
public class HobbyExt { String name int level String school HobbyExt(name) { this.name = name; } @Override public String toString() { return "My hobby is $name, level $level, School $school" } }

修改HelloPlugin.groovy代碼以下:

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
class HelloPlugin implements Plugin<Project> { public void apply(Project project) { NamedDomainObjectContainer<HobbyExt> hobbies = project.container(HobbyExt.class) project.extensions.add('hobbies', hobbies) project.task("sayHello") { group "hello" doLast { def hobbiesExt = project.hobbies def hobbiesExt1 = project.extensions.getByName('hobbies') println hobbiesExt println hobbiesExt1 } } } }

在使用插件的地方添加如下代碼:

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
hobbies { basketball { level = 4 school = "beijing" } football { level = 6 school = "qinghua" } }

執行 gradle sayHello獲得以下結果:

  
  
  
  
  • 1
  • 2
  • 3
:sayHello [My hobby is basketball, level 4, School beijing, My hobby is football, level 6, School qinghua] [My hobby is basketball, level 4, School beijing, My hobby is football, level 6, School qinghua]

到這裏說明咱們定義的Extension正確設置並讀取成功了。

3)狀況三

咱們也能夠混合列表和單個屬性,就像android{…}同樣
新建一個類
Team.groovy

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
public class TeamExt { NamedDomainObjectContainer<HobbyExt> hobbies String name int count public TeamExt(NamedDomainObjectContainer<HobbyExt> hobbies) { this.hobbies = hobbies } def hobbies(Closure closure) { hobbies.configure(closure) } String toString() { "this is a team, name: $name, count $count, hobbies: $hobbies" } }

這裏的hobbies(closure)函數是必須的,只有實現了這個函數,Gradle在解析team的extension,遇到hobbies配置時,才能經過調用函數,調用 NamedDomainObjectContainer的configure方法,往裏面添加對象。
接着咱們修改
HelloPlugin.groovy

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
class HelloPlugin implements Plugin<Project> { public void apply(Project project) { NamedDomainObjectContainer<HobbyExt> hobbyExt = project.container(HobbyExt) def team = new TeamExt(hobbyExt) project.extensions.add("team", team) project.task("sayHello") { group "hello" doLast { def teamExt = project.team def teamExt1 = project.extensions.getByName('team') println teamExt println teamExt1 } } } }

在使用插件的地方添加如下代碼:

  
  
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
team { name = "android" count = 10 hobbies { basketball { level = 4 school = "beijing" } football { level = 6 school = "qinghua" } } }

執行 gradle sayHello獲得以下結果:

  
  
  
  
  • 1
  • 2
  • 3
  • 4
:sayHello this is a team, name: android, count 10, hobbies: [My hobby is basketball, level 4, School beijing, My hobby is football, level 6, School qinghua] this is a team, name: android, count 10, hobbies: [My hobby is basketball, level 4, School beijing, My hobby is football, level 6, School qinghua]

到這裏說明咱們定義的Extension正確設置並讀取成功了。

以上的寫法是否是特別像build.gradle文件中的android標籤,他的內部能夠配置不少屬性,原理都和這個同樣。

到這裏,咱們就經過一個簡單的例子就熟悉了Gradle插件的編寫規則,並且經過對groovy語法的瞭解,讓咱們對gradle的DSL再也不陌生。

0x06 Groovy和Gradle的調試

主要參考 Small 中 Debug gradle on android studio

核心就是下面兩個命令:

  
  
  
  
  • 1
  • 2
  • 3
export GRADLE_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005" ./gradlew someTask -Dorg.gradle.daemon=false

其中有如下幾個注意事項:

- 對於項目根目錄下的gradle.properties文件須要作修改,必定要將org.gradle.jvmargs這個參數給註釋掉,緣由是上述在配置remote調試參數的時候已經配置了jvmargs,若是此時配置文件中仍然配置,會致使remote失效,從而不能監聽端口
- 在執行./gradlew someTask -Dorg.gradle.daemon=false這行命令時,爲了方即可以省略後面的-D參數,改成在配置文件中增長上述配置。這是-D參數的描述

若是你以爲好,對你有過幫助,請給我一點打賞鼓勵吧,一分也是愛呀!

相關文章
相關標籤/搜索