Groovy&Gradle總結


歡迎大家加入QQ群一起討論: 489873144(android格調小窩)
我的github地址:https://github.com/jeasonlzy

0x01 Groovy 概述

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

正因為如此,所以Groovy適合用來定義DSL(Domain Specific Language)。

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

0x02 groovy 基本知識

1)首先需要安裝groovy環境,具體的環境安裝就不說了,網上很多,安裝完成后配置環境變量,出現以下結果,即安裝成功

2)groovy與java

因為Groovy是基於JVM的語言,所以我們來看看最后生成的字節碼文件。我們寫一個類:
hello.groovy

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

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

上面的操作做完后,有什么感覺和體會?
最大的感覺可能就是groovy和shell腳本,或者python好類似。
另外,除了可以直接使用JDK之外,Groovy還有自己的一套GDK。

我們看一下編譯成jvm字節碼后的結果

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

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 ", ""});
    }
}
  
  
 
 
         
  • 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

到這里我們可以發現,其實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()方法無法直接訪問它。

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

我們將修改后的代碼編譯成class文件后,與之前的正常結果做對比,發現以下不同:

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

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

但是這樣還是與我們的想象有差距,name並沒有在成員位置,那如何才能才能讓我們定義的屬性就生成在成員變量的位置呢?這時候需要@Field注解,如下:

import groovy.transform.Field

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

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

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 ", ""});
    }
}
  
  
 
 
         
  • 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

0x03 Groovy 語法

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

1)方法的輸入參數優化

groovy中定義的函數,如果至少有一個參數,在調用的時候可以省略括號。如果某個函數沒有參數,那就不能省略括號,否則會當成一個變量使用。

def func(String a){
    println(a)
}

func 'hello'
  
  
 
 
         
  • 1
  • 2
  • 3
  • 4
  • 5

在android項目中,比如build.gradle

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

比如這里compileSdkVersion 和 buildToolsVersion 其實就是調用了同樣名字的兩個函數,在AndroidStudio里面可以點進去查看函數實現

2)閉包

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

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

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"
}
  
  
 
 
         
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

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

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

我們編寫如下代碼:test1.groovy

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
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

運行后得到如下結果:

--------------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
  
  
 
 
         
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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

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

運行后得到如下結果:

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

這里重點強調一下,成員變量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中加入以下幾行代碼:

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

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

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

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;
    }

    ···
}
  
  
 
 
         
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

5)操作符重載

我們經常在gradle中看見以下代碼:

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

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

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

因為task重載了leftShift,所以可以使用 << 操作符,這和c++的特性是一樣的

6)Command Chains

這個特性不僅可以省略函數調用中的括號,而且可以省略,連續函數調用中的. 點號, 比如
a(b).c(d) 這里a c是函數, b d是函數參數, 就可以縮寫為a b c d。這個特性強大之處在於不僅適用於單個參數類型函數,而且適用於多個參數類型的函數,當參數類型為閉包時同樣適用。

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

7)DSL

借助閉包的特性,我們可以嘗試寫一個簡單的DSL。下面的代碼展示了如何借助groovy的語法特性來實現一個DSL,這些特性我們稍后會在gradle的腳本中看到。

test3.groovy

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
}
  
  
 
 
         
  • 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

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

0x04 Gradle 基本概念

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

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

如果是單工程結構,這個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的東西。

apply plugin: 'com.android.application' //app插件
apply plugin: 'com.android.library'     //lib插件

android{
    ...
}  
dependencies{
  ....
}
  
  
 
 
         
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

如果我們把以上代碼在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對像

其中有很多有用的方法:

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

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

屬性可見范圍

  • 1.Project 本身
  • 2.Project的ext屬性
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
  
  
 
 
         
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 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的語法為:

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

這句話應該這么理解:

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

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

task <taskName> {
}
  
  
 
 
         
  • 1
  • 2

少了這個<< 操作符,意思就完全不一樣了,這個時候調用的函數為

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

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

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

當然我們還可以這樣指定task的行為:

task exampleTask {
    doLast{

    }
}

task exampleTask doLast{

}

task exampleTask << {

}
  
  
 
 
         
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

還可以指定task的類型

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

gradle內置為我們生成了很多task類型,比如Copy,Delete,可以點擊鏈接 查看gradle內置的task類型列表,如果創建task時沒有指定type,則他默認是DefaultTask類型。我們還可以創建自己的task類型,我們在稍后就會講到。

我們還可以可以指定task之間的依賴關系, 通過dependsOn, mustRunAfter, shouldRunAfter來指定。 還可以指定task的分組group, 如果不指定,將會出現在other里面。

5) 構建的生命周期測試

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

├── app
│   └── build.gradle
├── lib
│   └── build.gradle
├── build.gradle
└── settings.gradle

  
  
 
 
         
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

root/settings.gradle

println '------setting.gradle execute------'

include ':app', ':lib', ':helloPlugin', ':simplePlugin'
  
  
 
 
         
  • 1
  • 2
  • 3

root/build.gradle

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"
    }
}
  
  
 
 
         
  • 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

app/build.gradle

println '------app build.gradle execute------'

beforeEvaluate { project ->
    println "beforeEvaluate: ${project.name} --in--"
}

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

lib/build.gradle

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

在根目錄下執行 gradle libTask2

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

從上述例子中我們驗證了以下結果:

  1. 如果一個task在build.gradle中定義,但是在構建中不會執行,那么它的Task對象會創建,但是不會在任務圖中出現。
  2. 我們可以通過Gradle或者Project對象中定義的方法獲取生命周期中每一個過程在執行中的回調。這里注意一下,我們定義的一些回調在實際執行中似乎並沒有被觸發,例如,settingsEvaluated,projectsLoaded。具體原因需要細看。

0x05 自定義一個插件

首先看一下工程結構

├── 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
  
  
 
 
         
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

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

apply plugin: 'helloPlugin'
  
  
 
 
         
  • 1

這個文件里面內容指明了插件類的全類名,如下:

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

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

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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

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

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

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

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')
    }
}
  
  
 
 
         
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

有時我們更想開源出去給其他人用,像Small這樣,我們就可以這么寫
build.gradle

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'
}
  
  
 
 
         
  • 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

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引入插件

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

然后,在app工程下,我們應用這個插件,

apply plugin: 'helloPlugin'
  
  
 
 
         
  • 1

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

以上就是一個自定義插件的創建和應用過程,雖然很簡單,但是可以幫助我們理解gradle是如何通過plugin完成很多復雜的工作的。

Extension

1)情況一:

有時候我們希望在使用插件的時候,額外配置一些參數,這時候就需要額外寫一個Ext類,如下:
PersonExt.groovy

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")
    }
}
  
  
 
 
         
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

接着修改
HelloPlugin.groovy

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
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

最后我們再使用插件的地方添加以下屬性:

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

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

執行 gradle sayHello得到如下結果:

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

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

2)情況二

如果我們希望設置的Extension是一個集合列表,並且該列表長度未知,又該怎么寫呢?

我們需要使用NamedDomainObjectContainer,我們后面都簡稱NDOC 這是一個容納object的容器,它的特點是它的內部使用SortedSet實現的,內部對象的name是unique的,而且是按name進行排序的。通常創建NDOC的方法就是調用Project里的方法:

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

新增一個類:
HobbyExt.groovy

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"
    }
}
  
  
 
 
         
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

修改HelloPlugin.groovy代碼如下:

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
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

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

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

執行 gradle sayHello得到如下結果:

: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]
  
  
 
 
         
  • 1
  • 2
  • 3

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

3)情況三

我們也可以混合列表和單個屬性,就像android{…}一樣
新建一個類
Team.groovy

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"
    }
}
  
  
 
 
         
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

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

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
  • 15
  • 16
  • 17
  • 18

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

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

執行 gradle sayHello得到如下結果:

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

  
  
 
 
         
  • 1
  • 2
  • 3
  • 4

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

以上的寫法是不是特別像build.gradle文件中的android標簽,他的內部可以配置很多屬性,原理都和這個一樣。

到這里,我們就通過一個簡單的例子就熟悉了Gradle插件的編寫規則,而且通過對groovy語法的了解,讓我們對gradle的DSL不再陌生。

0x06 Groovy和Gradle的調試

主要參考 Small 中 Debug gradle on android studio

核心就是下面兩個命令:

export GRADLE_OPTS="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005"

./gradlew someTask -Dorg.gradle.daemon=false
  
  
 
 
         
  • 1
  • 2
  • 3

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

- 對於項目根目錄下的gradle.properties文件需要做修改,一定要將org.gradle.jvmargs這個參數給注釋掉,原因是上述在配置remote調試參數的時候已經配置了jvmargs,如果此時配置文件中仍然配置,會導致remote失效,從而不能監聽端口
- 在執行./gradlew someTask -Dorg.gradle.daemon=false這行命令時,為了方便可以省略后面的-D參數,改為在配置文件中增加上述配置。這是-D參數的描述

如果你覺得好,對你有過幫助,請給我一點打賞鼓勵吧,一分也是愛呀!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM