Gradle史上最詳細解析
前言
對於Android工程師來說編譯/打包等問題立即就成痛點了。一個APP有多個版本,Release版、Debug版、Test版。甚至針對不同APP Store都有不同的版本。在以前ROM的環境下,雖然可以配置Android.mk,但是需要依賴整個Android原始碼,而且還不能完全做到滿足條件,很多事情需要手動搞。一個app如果涉及到多個開發者,手動操作必然會帶來混亂。library工程我們需要編譯成jar包,然後釋出給其他開發者使用。以前是用eclipse的export,做一堆選擇。要是能自動編譯成jar包就爽了。上述問題對絕大部分APP開發者而言都不陌生,而Gradle作為一種很方便的的構建工具,可以非常輕鬆得解決構建過程中的各種問題。
構建
構建,叫build也好,叫make也行。反正就是根據輸入資訊然後幹一堆事情,最後得到幾個產出物(Artifact)。最最簡單的構建工具就是make了。make就是根據Makefile檔案中寫的規則,執行對應的命令,然後得到目標產物。日常生活中,和構建最類似的一個場景就是做菜。輸入各種食材,然後按固定的工序,最後得到一盤菜。當然,做同樣一道菜,由於需求不同,做出來的東西也不盡相同。比如,宮保雞丁這道菜,回民要求不能放大油、口淡的要求少放鹽和各種油、辣不怕的男女漢子們可以要求多放辣子....總之,做菜包含固定的工序,但是對於不同條件或需求,需要做不同的處理。
在Gradle爆紅之前,常用的構建工具是ANT,然後又進化到Maven。ANT和Maven這兩個工具其實也還算方便,現在還有很多地方在使用。但是二者都有一些缺點,所以讓更懶得人覺得不是那麼方便。比如,Maven編譯規則是用XML來編寫的。XML雖然通俗易懂,但是很難在xml中描述if{某條件成立,編譯某檔案}/else{編譯其他檔案}這樣有不同條件的任務。
怎麼解決?怎麼解決好?對程式設計師而言,自然是程式設計解決,但是有幾個小要求:
- 這種“程式設計”不要搞得和程式設計師理解的程式設計那樣複雜。寥寥幾筆,輕輕鬆鬆把要做的事情描述出來就最好不過。所以,Gradle選擇了Groovy。Groovy基於Java並拓展了Java。 Java程式設計師可以無縫切換到使用Groovy開發程式。Groovy說白了就是把寫Java程式變得像寫指令碼一樣簡單。寫完就可以執行,Groovy內部會將其編譯成Java class然後啟動虛擬機器來執行。當然,這些底層的渣活不需要你管。
- 除了可以用很靈活的語言來寫構建規則外,Gradle另外一個特點就是它是一種DSL,即Domain Specific Language,領域相關語言。什麼是DSL,說白了它是某個行業中的行話。還是不明白?徐克導演得《智取威虎山》中就有很典型的DSL使用描述,比如:
-
土匪:蘑菇,你哪路?什麼價?(什麼人?到哪裡去?) 楊子榮:哈!想啥來啥,想吃奶來了媽媽,想孃家的人,孩子他舅舅來了。(找同行) 楊子榮:拜見三爺! 土匪:天王蓋地虎!(你好大的膽!敢來氣你的祖宗?) 楊子榮:寶塔鎮河妖!(要是那樣,叫我從山上摔死,掉河裡淹死。) 土匪:野雞悶頭鑽,哪能上天王山!(你不是正牌的。) 楊子榮:地上有的是米,喂呀,有根底!(老子是正牌的,老牌的。)
Gradle中也有類似的行話,比如sourceSets代表原始檔的集合等.....太多了,記不住。以後我們都會接觸到這些行話。那麼,對使用者而言,這些行話的好處是什麼呢?這就是:一句行話可以包含很多意思,而且在這個行當裡的人一聽就懂,不用解釋。另外,基於行話,我們甚至可以建立一個模板,使用者只要往這個模板裡填必須要填的內容,Gradle就可以非常漂亮得完成工作,得到想要的東西。這就和現在的智慧炒菜機器似的,只要選擇菜譜,把食材準備好,剩下的事情就不用你操心了。吃貨們對這種做菜方式肯定是以反感為主,太沒有特色了。但是程式設計師對Gradle類似做法卻熱烈擁抱。到此,大家應該明白要真正學會Gradle恐怕是離不開下面兩個基礎知識:
- Groovy,由於它基於Java,所以我們僅介紹Java之外的東西。瞭解Groovy語言是掌握Gradle的基礎。
- Gradle作為一個工具,它的行話和它“為人處事”的原則。
Groovy介紹
Groovy是一種動態語言。這種語言比較有特點,它和Java一樣,也運行於Java虛擬機器中。簡單粗暴點兒看,你可以認為Groovy擴充套件了Java語言。比如,Groovy對自己的定義就是:Groovy是在 java平臺上的、 具有像Python, Ruby 和 Smalltalk 語言特性的靈活動態語言, Groovy保證了這些特性像 Java語法一樣被 Java開發者使用。除了語言和Java相通外,Groovy有時候又像一種指令碼語言。前文也提到過,當我執行Groovy指令碼時,Groovy會先將其編譯成Java類位元組碼,然後通過Jvm來執行這個Java類。圖1展示了Java、Groovy和Jvm之間的關係。
實際上,由於Groovy Code在真正執行的時候已經變成了Java位元組碼,所以JVM根本不知道自己執行的是Groovy程式碼。下面我們將介紹Groovy。由於此文的主要目的是Gradle,所以我們不會過多討論Groovy中細枝末節的東西,而是把知識點集中在以後和Gradle打交道時一些常用的地方上。
Groovy開發環境
在學習本節的時候,最好部署一下Groovy開發環境。根據Groovy官網的介紹,部署Groovy開發環境非常簡單,在Ubuntu或者cygwin之類的地方:
- curl -s get.gvmtool.net | bash
- source "$HOME/.gvm/bin/gvm-init.sh"
- gvm install groovy
- 執行完最後一步,Groovy就下載並安裝了。
然後,建立一個test.groovy檔案,裡邊只有一行程式碼:
println "hello groovy"
- 執行groovy test.groovy,輸出結果如圖2所示:
親們,必須要完成上面的操作啊。做完後,有什麼感覺和體會?
最大的感覺可能就是groovy和shell指令碼,或者python好類似。
另外,除了可以直接使用JDK之外,Groovy還有一套GDK。
說實話,看了這麼多家API文件,還是Google的Android API文件做得好。其頁面中右上角有一個搜尋欄,在裡邊輸入一些關鍵字,瞬間就能列出候選類,相關文件,方便得不得了啊.....
一些基礎的Groovy知識
為了後面講述方面,這裡先介紹一些前提知識。初期接觸可能有些彆扭,看習慣就好了。
- Groovy註釋標記和Java一樣,支援//或者/**/
- Groovy語句可以不用分號結尾。Groovy為了儘量減少程式碼的輸入,確實煞費苦心
- Groovy中支援動態型別,即定義變數的時候可以不指定其型別。Groovy中,變數定義可以使用關鍵字def。注意,雖然def不是必須的,但是為了程式碼清晰,建議還是使用def關鍵字
def variable1 = 1 //可以不使用分號結尾 def varable2 = "I am a person" def int x = 1 //變數定義時,也可以直接指定型別
- 函式定義時,引數的型別也可以不指定。比如
String testFunction(arg1,arg2){//無需指定引數型別 ... }
- 除了變數定義可以不指定型別外,Groovy中函式的返回值也可以是無型別的。比如:
//無型別的函式定義,必須使用def關鍵字
def nonReturnTypeFunc(){ last_line //最後一行程式碼的執行結果就是本函式的返回值 } //如果指定了函式返回型別,則可不必加def關鍵字來定義函式 String getString(){ return "I am a string" }
其實,所謂的無返回型別的函式,我估計內部都是按返回Object型別來處理的。畢竟,Groovy是基於Java的,而且最終會轉成Java Code執行在JVM上
- 函式返回值:Groovy的函式裡,可以不使用return xxx來設定xxx為函式返回值。如果不使用return語句的話,則函式裡最後一句程式碼的執行結果被設定成返回值。比如
//下面這個函式的返回值是字串"getSomething return value" def getSomething(){ "getSomething return value" //如果這是最後一行程式碼,則返回型別為String 1000 //如果這是最後一行程式碼,則返回型別為Integer }
注意,如果函式定義時候指明瞭返回值型別的話,函式中則必須返回正確的資料型別,否則執行時報錯。如果使用了動態型別的話,你就可以返回任何型別了。
- Groovy對字串支援相當強大,充分吸收了一些指令碼語言的優點:
1 單引號''中的內容嚴格對應Java中的String,不對$符號進行轉義
def singleQuote='I am $ dolloar' //輸出就是I am $ dolloar
2 雙引號""的內容則和指令碼語言的處理有點像,如果字元中有$號的話,則它會$表示式先求值。
def doubleQuoteWithoutDollar = "I am one dollar" //輸出 I am one dollar def x = 1 def doubleQuoteWithDollar = "I am $x dolloar" //輸出I am 1 dolloar
3 三個引號'''xxx'''中的字串支援隨意換行 比如
def multieLines = ''' begin line 1 line 2 end '''
- 最後,除了每行程式碼不用加分號外,Groovy中函式呼叫的時候還可以不加括號。比如:
println("test") ---> println "test"
注意,雖然寫程式碼的時候,對於函式呼叫可以不帶括號,但是Groovy經常把屬性和函式呼叫混淆。比如
def getSomething(){ "hello" }
getSomething() //如果不加括號的話,Groovy會誤認為getSomething是一個變數。
所以,呼叫函式要不要帶括號,我個人意見是如果這個函式是Groovy API或者Gradle API中比較常用的,比如println,就可以不帶括號。否則還是帶括號。Groovy自己也沒有太好的辦法解決這個問題,只能兵來將擋水來土掩了。
好了,瞭解上面一些基礎知識後,我們再介紹點深入的內容。
3.3 Groovy中的資料型別
Groovy中的資料型別我們就介紹兩種和Java不太一樣的:
- 一個是Java中的基本資料型別。
- 另外一個是Groovy中的容器類。
- 最後一個非常重要的是閉包。
放心,這裡介紹的東西都很簡單
3.3.1 基本資料型別
作為動態語言,Groovy世界中的所有事物都是物件。所以,int,boolean這些Java中的基本資料型別,在Groovy程式碼中其實對應的是它們的包裝資料型別。比如int對應為Integer,boolean對應為Boolean。比如下圖中的程式碼執行結果:
圖4 int實際上是Integer
3.3.2 容器類
Groovy中的容器類很簡單,就三種:
- List:連結串列,其底層對應Java中的List介面,一般用ArrayList作為真正的實現類。
- Map:鍵-值表,其底層對應Java中的LinkedHashMap。
- Range:範圍,它其實是List的一種拓展。
對容器而言,我們最重要的是瞭解它們的用法。下面是一些簡單的例子:
1. List類
變數定義:List變數由[]定義,比如
def aList = [5,'string',true] //List由[]定義,其元素可以是任何物件
變數存取:可以直接通過索引存取,而且不用擔心索引越界。如果索引超過當前連結串列長度,List會自動
往該索引新增元素
assert aList[1] == 'string' assert aList[5] == null //第6個元素為空 aList[100] = 100 //設定第101個元素的值為10 assert aList[100] == 100 //那麼,aList到現在為止有多少個元素呢? println aList.size ===>結果是101
2. Map類
容器變數定義
變數定義:Map變數由[:]定義,比如
def aMap = ['key1':'value1','key2':true]
Map由[:]定義,注意其中的冒號。冒號左邊是key,右邊是Value。key必須是字串,value可以是任何物件。另外,key可以用''或""包起來,也可以不用引號包起來。比如
def aNewMap = [key1:"value",key2:true] //其中的key1和key2預設被處理成字串"key1"和"key2" //不過Key要是不使用引號包起來的話,也會帶來一定混淆,比如 def key1="wowo" def aConfusedMap=[key1:"who am i?"] //aConfuseMap中的key1到底是"key1"還是變數key1的值“wowo”?顯然,答案是字串"key1"。如果要是"wowo"的話,則aConfusedMap的定義必須設定成: def aConfusedMap=[(key1):"who am i?"] //Map中元素的存取更加方便,它支援多種方法: println aMap.keyName //<==這種表達方法好像key就是aMap的一個成員變數一樣 println aMap['keyName'] //<==這種表達方法更傳統一點 aMap.anotherkey = "i am map" // <==為map新增新元素
3. Range類
Range是Groovy對List的一種拓展,變數定義和大體的使用方法如下:
def aRange = 1..5 //<==Range型別的變數 由begin值+兩個點+end值表示 //左邊這個aRange包含1,2,3,4,5這5個值 //如果不想包含最後一個元素,則 def aRangeWithoutEnd = 1..<5 <==包含1,2,3,4這4個元素 println aRange.from println aRange.to
3.3.4 Groovy API的一些祕笈
前面講這些東西,主要是讓大家瞭解Groovy的語法。實際上在coding的時候,是離不開SDK的。由於Groovy是動態語言,所以要使用它的SDK也需要掌握一些小訣竅。
Groovy的API文件位於 http://www.groovy-lang.org/api.html
以上文介紹的Range為例,我們該如何更好得使用它呢?
- 先定位到Range類。它位於groovy.lang包中:
有了API文件,你就可以放心呼叫其中的函數了。不過,不過,不過:我們剛才程式碼中用到了Range.from/to屬性值,但翻看Range API文件的時候,其實並沒有這兩個成員變數。圖6是Range的方法
文件中並沒有說明Range有from和to這兩個屬性,但是卻有getFrom和getTo這兩個函式。What happened?原來:
根據Groovy的原則,如果一個類中有名為xxyyzz這樣的屬性(其實就是成員變數),Groovy會自動為它新增getXxyyzz和setXxyyzz兩個函式,用於獲取和設定xxyyzz屬性值。
注意,get和set後第一個字母是大寫的。所以,當你看到Range中有getFrom和getTo這兩個函式時候,就得知道潛規則下,Range有from和to這兩個屬性。當然,由於它們不可以被外界設定,所以沒有公開setFrom和setTo函式。
閉包
3.4.1 閉包的樣子
閉包,英文叫Closure,是Groovy中非常重要的一個數據型別或者說一種概念了。閉包的歷史來源,種種好處我就不說了。我們直接看怎麼使用它!
閉包,是一種資料型別,它代表了一段可執行的程式碼。其外形如下:
def aClosure = {//閉包是一段程式碼,所以需要用花括號括起來.. String param1, int param2 -> //這個箭頭很關鍵。箭頭前面是引數定義,箭頭後面是程式碼 println"this is code" //這是程式碼,最後一句是返回值, //也可以使用return,和Groovy中普通函式一樣 }
簡而言之,Closure的定義格式是:
def xxx = {paramters -> code} //或者 def xxx = {無引數,純code} 這種case不需要->符號
說實話,從C/C++語言的角度看,閉包和函式指標很像。閉包定義好後,要呼叫它的方法就是:
閉包物件.call(引數) 或者更像函式指標呼叫的方法:
閉包物件(引數)
比如:
aClosure.call("this is string",100) 或者 aClosure("this is string", 100)
上面就是一個閉包的定義和使用。在閉包中,還需要注意一點:
如果閉包沒定義引數的話,則隱含有一個引數,這個引數名字叫it,和this的作用類似。it代表閉包的引數。
比如:
def greeting = { "Hello, $it!" } assert greeting('Patrick') == 'Hello, Patrick!'
等同於:
def greeting = { it -> "Hello, $it!" } assert greeting('Patrick') == 'Hello, Patrick!'
但是,如果在閉包定義時,採用下面這種寫法,則表示閉包沒有引數!
def noParamClosure = { -> true }
這個時候,我們就不能給noParamClosure傳引數了!
noParamClosure ("test") <==報錯喔!
3.4.2 Closure使用中的注意點
1. 省略圓括號
閉包在Groovy中大量使用,比如很多類都定義了一些函式,這些函式最後一個引數都是一個閉包。比如:
public static <T> List<T> each(List<T> self, Closure closure)
上面這個函式表示針對List的每一個元素都會呼叫closure做一些處理。這裡的closure,就有點回調函式的感覺。但是,在使用這個each函式的時候,我們傳遞一個怎樣的Closure進去呢?比如:
def iamList = [1,2,3,4,5] //定義一個List iamList.each{ //呼叫它的each,這段程式碼的格式看不懂了吧?each是個函式,圓括號去哪了? println it }
上面程式碼有兩個知識點:
- each函式呼叫的圓括號不見了!原來,Groovy中,當函式的最後一個引數是閉包的話,可以省略圓括號。比如
def testClosure(int a1,String b1, Closure closure){ //do something closure() //呼叫閉包 } //那麼呼叫的時候,就可以免括號! testClosure (4, "test", { println "i am in closure" } ) //紅色的括號可以不寫..
注意,這個特點非常關鍵,因為以後在Gradle中經常會出現圖7這樣的程式碼:
經常碰見圖7這樣的沒有圓括號的程式碼。省略圓括號雖然使得程式碼簡潔,看起來更像指令碼語言,但是它這經常會讓我confuse(不知道其他人是否有同感),以doLast為例,完整的程式碼應該按下面這種寫法:
doLast({ println 'Hello world!' })
有了圓括號,你會知道 doLast只是把一個Closure物件傳了進去。很明顯,它不代表這段指令碼解析到doLast的時候就會呼叫println 'Hello world!' 。
但是把圓括號去掉後,就感覺好像println 'Hello world!'立即就會被呼叫一樣!
2. 如何確定Closure的引數
另外一個比較讓人頭疼的地方是,Closure的引數該怎麼搞?還是剛才的each函式:
public static <T> List<T> each(List<T> self, Closure closure)
如何使用它呢?比如:
def iamList = [1,2,3,4,5] //定義一個List變數 iamList.each{ //呼叫它的each函式,只要傳入一個Closure就可以了。 println it }
看起來很輕鬆,其實:
- 對於each所需要的Closure,它的引數是什麼?有多少個引數?返回值是什麼?
我們能寫成下面這樣嗎?
iamList.each{String name,int x -> return x } //執行的時候肯定報錯!
所以,Closure雖然很方便,但是它一定會和使用它的上下文有極強的關聯。要不,作為類似回撥這樣的東西,我如何知道呼叫者傳遞什麼引數給Closure呢?
此問題如何破解?只能通過查詢API文件才能瞭解上下文語義。比如下圖8:
圖8中:
- each函式說明中,將給指定的closure傳遞Set中的每一個item。所以,closure的引數只有一個。
- findAll中,絕對抓瞎了。一個是沒說明往Closure裡傳什麼。另外沒說明Closure的返回值是什麼.....。
對Map的findAll而言,Closure可以有兩個引數。findAll會將Key和Value分別傳進去。並且,Closure返回true,表示該元素是自己想要的。返回false表示該元素不是自己要找的。示意程式碼如圖9所示:
Closure的使用有點坑,很大程度上依賴於你對API的熟悉程度,所以最初階段,SDK查詢是少不了的。
3.5 指令碼類、檔案I/O和XML操作
最後,我們來看一下Groovy中比較高階的用法。
3.5.1 指令碼類
1. 指令碼中import其他類
Groovy中可以像Java那樣寫package,然後寫類。比如在資料夾com/cmbc/groovy/目錄中放一個檔案,叫Test.groovy,如圖10所示:
你看,圖10中的Test.groovy和Java類就很相似了。當然,如果不宣告public/private等訪問許可權的話,Groovy中類及其變數預設都是public的。
現在,我們在測試的根目錄下建立一個test.groovy檔案。其程式碼如下所示:
你看,test.groovy先import了com.cmbc.groovy.Test類,然後建立了一個Test型別的物件,接著呼叫它的print函式。
這兩個groovy檔案的目錄結構如圖12所示:
在groovy中,系統自帶會載入當前目錄/子目錄下的xxx.groovy檔案。所以,當執行groovy test.groovy的時候,test.groovy import的Test類能被自動搜尋並載入到。
2. 指令碼到底是什麼
Java中,我們最熟悉的是類。但是我們在Java的一個原始碼檔案中,不能不寫class(interface或者其他....),而Groovy可以像寫指令碼一樣,把要做的事情都寫在xxx.groovy中,而且可以通過groovy xxx.groovy直接執行這個指令碼。這到底是怎麼搞的?
既然是基於Java的,Groovy會先把xxx.groovy中的內容轉換成一個Java類。比如:
test.groovy的程式碼是:
println 'Groovy world!'
Groovy把它轉換成這樣的Java類:
執行 groovyc -d classes test.groovy
groovyc是groovy的編譯命令,-d classes用於將編譯得到的class檔案拷貝到classes資料夾下
圖13是test.groovy指令碼轉換得到的java class。用jd-gui反編譯它的程式碼:
圖13中:
- test.groovy被轉換成了一個test類,它從script派生。
- 每一個指令碼都會生成一個static main函式。這樣,當我們groovy test.groovy的時候,其實就是用java去執行這個main函式
- 指令碼中的所有程式碼都會放到run函式中。比如,println 'Groovy world',這句程式碼實際上是包含在run函式裡的。
- 如果指令碼中定義了函式,則函式會被定義在test類中。
groovyc是一個比較好的命令,讀者要掌握它的用法。然後利用jd-gui來檢視對應class的Java原始碼。
3. 指令碼中的變數和作用域
前面說了,xxx.groovy只要不是和Java那樣的class,那麼它就是一個指令碼。而且指令碼的程式碼其實都會被放到run函式中去執行。那麼,在Groovy的指令碼中,很重要的一點就是指令碼中定義的變數和它的作用域。舉例:
def x = 1 <==注意,這個x有def(或者指明型別,比如 int x = 1)
def printx(){
println x
}
printx() <==報錯,說x找不到
為什麼?繼續來看反編譯後的class檔案。
圖14中:
printx被定義成test類的成員函式
def x = 1,這句話是在run中建立的。所以,x=1從程式碼上看好像是在整個指令碼中定義的,但實際上printx訪問不了它。printx是test成員函式,除非x也被定義成test的成員函式,否則printx不能訪問它。
那麼,如何使得printx能訪問x呢?很簡單,定義的時候不要加型別和def。即:
x = 1 <==注意,去掉def或者型別
def printx(){
println x
}
printx() <==OK
這次Java原始碼又變成什麼樣了呢?
圖15中,x也沒有被定義成test的成員函式,而是在run的執行過程中,將x作為一個屬性新增到test例項物件中了。然後在printx中,先獲取這個屬性。
注意,Groovy的文件說 x = 1這種定義將使得x變成test的成員變數,但從反編譯情況看,這是不對的.....
雖然printx可以訪問x變量了,但是假如有其他指令碼卻無法訪問x變數。因為它不是test的成員變數。
比如,我在測試目錄下建立一個新的名為test1.groovy。這個test1將訪問test.groovy中定義的printx函式:
這種方法使得我們可以將程式碼分成模組來編寫,比如將公共的功能放到test.groovy中,然後使用公共功能的程式碼放到test1.groovy中。
執行groovy test1.groovy,報錯。說x找不到。這是因為x是在test的run函式動態加進去的。怎麼辦?
import groovy.transform.Field; //必須要先import
@Field x = 1 <==在x前面加上@Field標註,這樣,x就徹徹底底是test的成員變量了。
檢視編譯後的test.class檔案,得到:
這個時候,test.groovy中的x就成了test類的成員函數了。如此,我們可以在script中定義那些需要輸出給外部指令碼或類使用的變量了!
3.5.2 檔案I/O操作
本節介紹下Groovy的檔案I/O操作。直接來看例子吧,雖然比Java看起來簡單,但要理解起來其實比較難。尤其是當你要自己查SDK並編寫程式碼的時候。
整體說來,Groovy的I/O操作是在原有Java I/O操作上進行了更為簡單方便的封裝,並且使用Closure來簡化程式碼編寫。主要封裝瞭如下一些了類:
1. 讀檔案
Groovy中,檔案讀操作簡單到令人髮指:
def targetFile = new File(檔名) <==File物件還是要建立的。
然後開啟http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/File.html
看看Groovy定義的API:
1 讀該檔案中的每一行:eachLine的唯一引數是一個Closure。Closure的引數是檔案每一行的內容
其內部實現肯定是Groovy開啟這個檔案,然後讀取檔案的一行,然後呼叫Closure...
targetFile.eachLine{
StringoneLine ->
printlnoneLine
<==是不是令人髮指??!
2 直接得到檔案內容
targetFile.getBytes() <==檔案內容一次性讀出,返回型別為byte[]
注意前面提到的getter和setter函式,這裡可以直接使用targetFile.bytes //....
3 使用InputStream.InputStream的SDK在 http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/InputStream.html
def ism = targetFile.newInputStream()
//操作ism,最後記得關掉
ism.close
4 使用閉包操作inputStream,以後在Gradle裡會常看到這種搞法
targetFile.withInputStream{ ism ->
操作ism. 不用close。Groovy會自動替你close
}
確實夠簡單,令人髮指。我當年死活也沒找到withInputStream是個啥意思。所以,請各位開發者牢記Groovy I/O操作相關類的SDK地址:
- java.io.File: http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/File.html
- java.io.InputStream: http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/InputStream.html
- java.io.OutputStream: http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/OutputStream.html
- java.io.Reader: http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/Reader.html
- java.io.Writer: http://docs.groovy-lang.org/latest/html/groovy-jdk/java/io/Writer.html
- java.nio.file.Path: http://docs.groovy-lang.org/latest/html/groovy-jdk/java/nio/file/Path.html
2. 寫檔案
和讀檔案差不多。不再囉嗦。這裡給個例子,告訴大家如何copy檔案。
def srcFile = new File(原始檔名)
def targetFile = new File(目標檔名)
targetFile.withOutputStream{ os->
srcFile.withInputStream{ ins->
os << ins //利用OutputStream的<<操作符過載,完成從inputstream到OutputStream
//的輸出
}
}
關於OutputStream的<<操作符過載,檢視SDK文件後可知:
再一次向極致簡單致敬。但是,SDK恐怕是離不開手了...
3.5.3 XML操作
除了I/O異常簡單之外,Groovy中的XML操作也極致得很。Groovy中,XML的解析提供了和XPath類似的方法,名為GPath。這是一個類,提供相應API。關於XPath,請看Wiki。
GPath功能包括:給個例子好了,來自Groovy官方文件。
test.xml檔案:
<response version-api="2.0"> <value> <books> <book available="20" id="1"> <title>Don Xijote</title> <author id="1">Manuel De Cervantes</author> </book> <book available="14" id="2"> <title>Catcher in the Rye</title> <author id="2">JD Salinger</author> </book> <book available="13" id="3"> <title>Alice in Wonderland</title> <author id="3">Lewis Carroll</author> </book> <book available="5" id="4"> <title>Don Xijote</title> <author id="4">Manuel De Cervantes</author> </book> </books> </value> </response>
- 現在來看怎麼玩轉GPath:
//第一步,建立XmlSlurper類 def xparser = new XmlSlurper() def targetFile = new File("test.xml") //轟轟的GPath出場 GPathResult gpathResult =xparser.parse(targetFile) //開始玩test.xml。現在我要訪問id=4的book元素。 //下面這種搞法,gpathResult代表根元素response。通過e1.e2.e3這種 //格式就能訪問到各級子元素.... def book4 = gpathResult.value.books.book[3] //得到book4的author元素 def author = book4.author //再來獲取元素的屬性和textvalue assert author.text() == ' Manuel De Cervantes ' 獲取屬性更直觀 [email protected] == '4' 或者 author['@id'] == '4' //屬性一般是字串,可通過toInteger轉換成整數 [email protected]() == 4 //好了。GPath就說到這。再看個例子。我在使用Gradle的時候有個需求,就是獲取AndroidManifest.xml版本號(versionName)。有了GPath,一行程式碼搞定,請看: def androidManifest = newXmlSlurper().parse("AndroidManifest.xml") println androidManifest['@android:versionName'] //或者 println [email protected]'android:versionName'
3.6 更多
作為一門語言,Groovy是複雜的,是需要深入學習和鑽研的。一本厚書甚至都無法描述Groovy的方方面面。
Anyway,從使用角度看,尤其是又限定在Gradle這個領域內,能用到的都是Groovy中一些簡單的知識。
Gradle介紹
現在正式進入Gradle。Gradle是一個工具,同時它也是一個程式設計框架。前面也提到過,使用這個工具可以完成app的編譯打包等工作。當然你也可以用它幹其他的事情。
Gradle是什麼?學習它到什麼地步就可以了?
=====>看待問題的時候,所站的角度非常重要。
-->當你把Gradle當工具看的時候,我們只想著如何用好它。會寫、寫好配置指令碼就OK
-->當你把它當做程式設計框架看的時候,你可能需要學習很多更深入的內容。
另外,今天我們把它當工具看,明天因為需求發生變化,我們可能又得把它當程式設計框架看。
4.1 Gradle開發環境部署
Gradle的官網:http://gradle.org/
文件位置:https://docs.gradle.org/current/release-notes。其中的User Guide和DSL Reference很關鍵。User Guide就是介紹Gradle的一本書,而DSL Reference是Gradle API的說明。
以Ubuntu為例,下載Gradle:http://gradle.org/gradle-download/ 選擇Complete distribution和Binary only distribution都行。然後解壓到指定目錄。
最後,設定~/.bashrc,把Gradle加到PATH裡,如圖20所示:
執行source ~/.bashrc,初始化環境。
執行gradle --version,如果成功執行就OK了。
注意,為什麼說Gradle是一個程式設計框架?來看它提供的API文件:
https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html
原來,我們編寫所謂的編譯指令碼,其實就是玩Gradle的API....所以它從更底層意義上看,是一個程式設計框架!
既然是程式設計框架,我在講解Gradle的時候,儘量會從API的角度來介紹。有些讀者肯定會不耐煩,為嘛這麼費事?
從我個人的經歷來看:因為我從網上學習到的資料來看,幾乎全是從指令碼的角度來介紹Gradle,結果學習一通下來,只記住引數怎麼配置,卻不知道它們都是函式呼叫,都是嚴格對應相關API的。
而從API角度來看待Gradle的話,有了SDK文件,你就可以程式設計。程式設計是靠記住一行行程式碼來實現的嗎?不是,是在你掌握大體流程,然後根據SDK+API來完成的!
其實,Gradle自己的User Guide也明確說了:
Build scripts are code
4.2 基本元件
Gradle是一個框架,它定義一套自己的遊戲規則。我們要玩轉Gradle,必須要遵守它設計的規則。下面我們來講講Gradle的基本元件:
Gradle中,每一個待編譯的工程都叫一個Project。每一個Project在構建的時候都包含一系列的Task。比如一個Android APK的編譯可能包含:Java原始碼編譯Task、資源編譯Task、JNI編譯Task、lint檢查Task、打包生成APK的Task、簽名Task等。
一個Project到底包含多少個Task,其實是由編譯指令碼指定的外掛決定。外掛是什麼呢?外掛就是用來定義Task,並具體執行這些Task的東西。
剛才說了,Gradle是一個框架,作為框架,它負責定義流程和規則。而具體的編譯工作則是通過外掛的方式來完成的。比如編譯Java有Java外掛,編譯Groovy有Groovy外掛,編譯Android APP有Android APP外掛,編譯Android Library有Android Library外掛
好了。到現在為止,你知道Gradle中每一個待編譯的工程都是一個Project,一個具體的編譯過程是由一個一個的Task來定義和執行的。
4.2.1 一個重要的例子
下面我們來看一個實際的例子。這個例子非常有代表意義。圖22是一個名為posdevice的目錄。這個目錄裡包含3個Android Library工程,2個Android APP工程。
在圖22的例子中:
- CPosDeviceSdk、CPosSystemSdk、CPosSystemSdkxxxImpl是Android Library。其中,CPosSystemSdkxxxImpl依賴CPosSystemSdk
- CPosDeviceServerApk和CPosSdkDemo是Android APP。這些App和SDK有依賴關係。CPosDeviceServerApk依賴CPosDeviceSdk,而CPosSdkDemo依賴所有的Sdk Library。
請回答問題,在上面這個例子中,有多少個Project?
答案是:每一個Library和每一個App都是單獨的Project。根據Gradle的要求,每一個Project在其根目錄下都需要有一個build.gradle。build.gradle檔案就是該Project的編譯指令碼,類似於Makefile。
看起來好像很簡單,但是請注意:posdevice雖然包含5個獨立的Project,但是要獨立編譯他們的話,得:
- cd 某個Project的目錄。比如 cd CPosDeviceSdk
- 然後執行 gradle xxxx(xxx是任務的名字。對Android來說,assemble這個Task會生成最終的產物,所以gradle assemble)
這很麻煩啊,有10個獨立Project,就得重複執行10次這樣的命令。更有甚者,所謂的獨立Project其實有依賴關係的。比如我們這個例子。
那麼,我想在posdevice目錄下,直接執行gradle assemble,是否能把這5個Project的東西都編譯出來呢?
答案自然是可以。在Gradle中,這叫Multi-Projects Build。把posdevice改造成支援Gradle的Multi-Projects Build很容易,需要:
- 在posdevice下也新增一個build.gradle。這個build.gradle一般幹得活是:配置其他子Project的。比如為子Project新增一些屬性。這個build.gradle有沒有都無所屬。
- 在posdevice下新增一個名為settings.gradle。這個檔案很重要,名字必須是settings.gradle。它裡邊用來告訴Gradle,這個multiprojects包含多少個子Project。
來看settings.gradle的內容,最關鍵的內容就是告訴Gradle這個multiprojects包含哪些子projects:
[settings.gradle]
//通過include函式,將子Project的名字(其資料夾名)包含進來 include 'CPosSystemSdk' ,'CPosDeviceSdk' , 'CPosSdkDemo','CPosDeviceServerApk','CPosSystemSdkWizarPosImpl'
強烈建議:
如果你確實只有一個Project需要編譯,我也建議你在目錄下新增一個settings.gradle。我們團隊內部的所有單個Project都已經改成支援Multiple-Project Build了。改得方法就是新增settings.gradle,然後include對應的project名字。
另外,settings.gradle除了可以include外,還可以設定一些函式。這些函式會在gradle構建整個工程任務的時候執行,所以,可以在settings做一些初始化的工作。比如:我的settings.gradle的內容:
//定義一個名為initMinshengGradleEnvironment的函式。該函式內部完成一些初始化操作 //比如建立特定的目錄,設定特定的引數等 def initMinshengGradleEnvironment(){ println"initialize Minsheng Gradle Environment ....." ......//幹一些special的私活.... println"initialize Minsheng Gradle Environment completes..." } //settings.gradle載入的時候,會執行initMinshengGradl