歡迎大家加入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,但是為什么可以呢?
我們需要在根目錄下也添加一個build.gradle。這個build.gradle一般干得活是:配置其他子Project的。比如為子Project添加一些屬性。這個build.gradle有沒有都無所謂。
繼續在根目錄下添加一個名為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的構建腳本生命周期具備三大步,如下:
上圖告訴我們以下信息,
- Gradle工作包含三個階段:
- 首先是初始化階段。對我們前面的multi-project build而言,就是執行settings.gradle
- Initiliazation phase的下一個階段是Configration階段。
- Configration階段的目標是解析每個project中的build.gradle。比如multi-project build例子中,
解析每個子目錄中的build.gradle。在這兩個階段之間,我們可以加一些定制化的Hook。這當然是通過
API來添加的,需要特別注意:每個Project都會被解析。 - Configuration階段完了后,整個build的project以及內部的Task關系就確定了。一個
Project包含很多Task,每個Task之間有依賴關系。Configuration會建立一個有向圖來描述Task之間的
依賴關系,是一個有向圖,所以,我們可以添加一個HOOK,即當Task關系圖建立好后,執行一些操作. - 最后一個階段就是執行任務了。你在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對象:
- 創建一個Settings對象,
- 根據settings.gradle文件配置它
- 根據Settings對象中定義的工程的父子關系創建Project對象
- 執行每一個工程的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屬性,是只讀的
當獲取或者設置這些屬性的時候,按照上述的順序依次尋找,如果都沒找到,則拋出異常。
方法可見范圍
- Project對象本身
- build.gradle文件中定義的方法
- 通過plugin添加的extension,每個extensions都可以作為一個方法訪問,它接受一個閉包或Action作為參數
- 通過plugin添加的方法。一個plugin可以通過Project的Convention對象為project添加屬性和方法。
- project中的task,每一個task 都會在當前project中存在一個接受一個閉包或者Action作為參數的方法,這個閉包會在task的configure(closure)方法中調用。
- 當前工程的父工程中的方法
- 當前工程的屬性可見范圍中所有的閉包屬性都可以作為方法訪問
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
這句話應該這么理解:
- 首先調用project的task方法,傳入一個taskName,返回一個task
- 調用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) 構建的生命周期測試
├── 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
從上述例子中我們驗證了以下結果:
- 如果一個task在build.gradle中定義,但是在構建中不會執行,那么它的Task對象會創建,但是不會在任務圖中出現。
- 我們可以通過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
參數的描述