格式更加精美的PDF版請到http://vdisk.weibo.com/s/z68f8l0xTYrZt 下載下傳
Gradle是目前非常“勁爆”得建構工具。本篇文章就是專為講解Gradle而來。介紹Gradle之前先說點題外話。
說實話我在大法工作的時候就見過Gradle。但是當時我一直不知道這是什麼東西。而且大法工具組的工程師還将其和Android Studio大法版一起推送偶一看就更沒興趣了。為什麼那個時候如此不待見Gradle呢因為我此前一直是做ROM開發。在這個層面上我們用makemm或者mmm就可以了。而且編譯耗時對我們來說也不是啥痛點因為用組内吊炸天的神機伺服器完整編譯大法的image也要耗費1個小時左右。是以那個時侯Gradle完全不是我們的菜。
現在搞APP開發居多編譯/打包等問題立即就成痛點了。比如
一個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内部會将其編譯成Javaclass然後啟動虛拟機來執行。當然這些底層的渣活不需要你管。
除了可以用很靈活的語言來寫建構規則外Gradle另外一個特點就是它是一種DSL即Domain Specific Language領域相關語言。什麼是DSL說白了它是某個行業中的行話。還是不明白徐克導演得《智取威虎山》中就有很典型的DSL使用描述比如
------------------------------------------------------------------------------
土匪蘑菇你哪路什麼價什麼人到哪裡去
楊子榮哈想啥來啥想吃奶來了媽媽想娘家的人孩子他舅舅來了。找同行
楊子榮拜見三爺
土匪天王蓋地虎你好大的膽敢來氣你的祖宗
楊子榮寶塔鎮河妖要是那樣叫我從山上摔死掉河裡淹死。
土匪野雞悶頭鑽哪能上天王山你不是正牌的。
楊子榮地上有的是米喂呀有根底老子是正牌的老牌的。
Gradle中也有類似的行話比如sourceSets代表源檔案的集合等.....太多了記不住。以後我們都會接觸到這些行話。那麼對使用者而言這些行話的好處是什麼呢這就是
一句行話可以包含很多意思而且在這個行當裡的人一聽就懂不用解釋。另外基于行話我們甚至可以建立一個模闆使用者隻要往這個模闆裡填必須要填的内容Gradle就可以非常漂亮得完成工作得到想要的東西。
這就和現在的智能炒菜機器似的隻要選擇菜單把食材準備好剩下的事情就不用你操心了。吃貨們對這種做菜方式肯定是以反感為主太沒有特色了。但是程式員對Gradle類似做法卻熱烈擁抱。
到此大家應該明白要真正學會Gradle恐怕是離不開下面兩個基礎知識
Groovy由于它基于Java是以我們僅介紹Java之外的東西。了解Groovy語言是掌握Gradle的基礎。
Gradle作為一個工具它的行話和它“為人處事”的原則。
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之間的關系。

圖1 Java、Groovy和JVM的關系
實際上由于Groovy Code在真正執行的時候已經變成了Java位元組碼是以JVM根本不知道自己運作的是Groovy代碼。
下面我們将介紹Groovy。由于此文的主要目的是Gradle是以我們不會過多讨論Groovy中細枝末節的東西而是把知識點集中在以後和Gradle打交道時一些常用的地方上。
在學習本節的時候最好部署一下Groovy開發環境。根據Groovy官網的介紹http://www.groovy-lang.org/download.html#gvm部署Groovy開發環境非常簡單在Ubuntu或者cygwin之類的地方
curl -s get.gvmtool.net | bash
source"$HOME/.gvm/bin/gvm-init.sh"
gvm install groovy
執行完最後一步Groovy就下載下傳并安裝了。圖1是安裝時候的示意圖
圖1 Groovy安裝示意圖
然後建立一個test.groovy檔案裡邊隻有一行代碼
println "hello groovy"
執行groovy test.groovy輸出結果如圖2所示
圖2 執行groovy腳本
親們必須要完成上面的操作啊。做完後有什麼感覺和體會
最大的感覺可能就是groovy和shell腳本或者python好類似。
另外除了可以直接使用JDK之外Groovy還有一套GDK網址是http://www.groovy-lang.org/api.html。
說實話看了這麼多家API文檔還是Google的Android API文檔做得好。其頁面中右上角有一個搜尋欄在裡邊輸入一些關鍵字瞬間就能列出候選類相關文檔友善得不得了啊.....
為了後面講述方面這裡先介紹一些前提知識。初期接觸可能有些别扭看習慣就好了。
l Groovy注釋标記和Java一樣支援//或者/**/
l Groovy語句可以不用分号結尾。Groovy為了盡量減少代碼的輸入确實煞費苦心
l Groovy中支援動态類型即定義變量的時候可以不指定其類型。Groovy中變量定義可以使用關鍵字def。注意雖然def不是必須的但是為了代碼清晰建議還是使用def關鍵字
def variable1 = 1 //可以不使用分号結尾
def varable2 = "I ama person"
def int x = 1 //變量定義時也可以直接指定類型
l 函數定義時參數的類型也可以不指定。比如
String testFunction(arg1,arg2){//無需指定參數類型
...
}
l 除了變量定義可以不指定類型外Groovy中函數的傳回值也可以是無類型的。比如
//無類型的函數定義必須使用def關鍵字
def nonReturnTypeFunc(){
last_line //最後一行代碼的執行結果就是本函數的傳回值
//如果指定了函數傳回類型則可不必加def關鍵字來定義函數
String getString(){
return"I am a string"
其實所謂的無傳回類型的函數我估計内部都是按傳回Object類型來處理的。畢竟Groovy是基于Java的而且最終會轉成Java Code運作在JVM上
l 函數傳回值Groovy的函數裡可以不使用returnxxx來設定xxx為函數傳回值。如果不使用return語句的話則函數裡最後一句代碼的執行結果被設定成傳回值。比如
//下面這個函數的傳回值是字元串"getSomething return value"
def getSomething(){
"getSomething return value" //如果這是最後一行代碼則傳回類型為String
1000//如果這是最後一行代碼則傳回類型為Integer
注意如果函數定義時候指明了傳回值類型的話函數中則必須傳回正确的資料類型否則運作時報錯。如果使用了動态類型的話你就可以傳回任何類型了。
l Groovy對字元串支援相當強大充分吸收了一些腳本語言的優點
1 單引号''中的内容嚴格對應Java中的String不對$符号進行轉義
defsingleQuote='I am $ dolloar' //輸出就是I am $ dolloar
2 雙引号""的内容則和腳本語言的處理有點像如果字元中有$号的話則它會$表達式先求值。
defdoubleQuoteWithoutDollar = "I am one dollar" //輸出 I am one dollar
def x = 1
defdoubleQuoteWithDollar = "I am $x dolloar" //輸出I am 1 dolloar
3 三個引号'''xxx'''中的字元串支援随意換行 比如
defmultieLines = ''' begin
line 1
line 2
end '''
l 最後除了每行代碼不用加分号外Groovy中函數調用的時候還可以不加括号。比如
println("test") ---> println"test"
注意雖然寫代碼的時候對于函數調用可以不帶括号但是Groovy經常把屬性和函數調用混淆。比如
"hello"
getSomething() //如果不加括号的話Groovy會誤認為getSomething是一個變量。比如
圖3 錯誤示意
是以調用函數要不要帶括号我個人意見是如果這個函數是Groovy API或者Gradle API中比較常用的比如println就可以不帶括号。否則還是帶括号。Groovy自己也沒有太好的辦法解決這個問題隻能兵來将擋水來土掩了。
好了了解上面一些基礎知識後我們再介紹點深入的内容。
Groovy中的資料類型我們就介紹兩種和Java不太一樣的
一個是Java中的基本資料類型。
另外一個是Groovy中的容器類。
最後一個非常重要的是閉包。
放心這裡介紹的東西都很簡單
作為動态語言Groovy世界中的所有事物都是對象。是以intboolean這些Java中的基本資料類型在Groovy代碼中其實對應的是它們的包裝資料類型。比如int對應為Integerboolean對應為Boolean。比如下圖中的代碼執行結果
圖4 int實際上是Integer
Groovy中的容器類很簡單就三種
l List連結清單其底層對應Java中的List接口一般用ArrayList作為真正的實作類。
l Map鍵-值表其底層對應Java中的LinkedHashMap。
l Range範圍它其實是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
容器變量定義
變量定義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添加新元素
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
前面講這些東西主要是讓大家了解Groovy的文法。實際上在coding的時候是離不開SDK的。由于Groovy是動态語言是以要使用它的SDK也需要掌握一些小訣竅。
Groovy的API文檔位于http://www.groovy-lang.org/api.html
以上文介紹的Range為例我們該如何更好得使用它呢
l 先定位到Range類。它位于groovy.lang包中
圖5 Range類API文檔
有了API文檔你就可以放心調用其中的函數了。不過不過不過我們剛才代碼中用到了Range.from/to屬性值但翻看Range API文檔的時候其實并沒有這兩個成員變量。圖6是Range的方法
圖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函數。
閉包英文叫Closure是Groovy中非常重要的一個資料類型或者說一種概念了。閉包的曆史來源種種好處我就不說了。我們直接看怎麼使用它
閉包是一種資料類型它代表了一段可執行的代碼。其外形如下
def aClosure = {//閉包是一段代碼是以需要用花括号括起來..
Stringparam1, int param2 -> //這個箭頭很關鍵。箭頭前面是參數定義箭頭後面是代碼
println"this is code" //這是代碼最後一句是傳回值
//也可以使用return和Groovy中普通函數一樣
簡而言之Closure的定義格式是
<pre name="code" class="java">def xxx = {paramters -> code} //或者
def xxx = {無參數純code} 這種case不需要->符号</pre><br><br>
說實話從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!"}
但是如果在閉包定義時采用下面這種寫法則表示閉包沒有參數
def noParamClosure = { -> true }
這個時候我們就不能給noParamClosure傳參數了
noParamClosure ("test") <==報錯喔
閉包在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
上面代碼有兩個知識點
l each函數調用的圓括号不見了原來Groovy中當函數的最後一個參數是閉包的話可以省略圓括号。比如
def testClosure(int a1,String b1, Closure closure){
//dosomething
closure() //調用閉包
那麼調用的時候就可以免括号
testClosure (4, "test", {
println"i am in closure"
} ) //紅色的括号可以不寫..
注意這個特點非常關鍵因為以後在Gradle中經常會出現圖7這樣的代碼
圖7 閉包調用
經常碰見圖7這樣的沒有圓括号的代碼。省略圓括号雖然使得代碼簡潔看起來更像腳本語言但是它這經常會讓我confuse不知道其他人是否有同感以doLast為例完整的代碼應該按下面這種寫法
doLast({
println'Hello world!'
})
有了圓括号你會知道 doLast隻是把一個Closure對象傳了進去。很明顯它不代表這段腳本解析到doLast的時候就會調用println 'Hello world!' 。
但是把圓括号去掉後就感覺好像println 'Hello world!'立即就會被調用一樣
另外一個比較讓人頭疼的地方是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
看起來很輕松其實
l 對于each所需要的Closure它的參數是什麼有多少個參數傳回值是什麼
我們能寫成下面這樣嗎
iamList.each{String name,int x ->
return x
} //運作的時候肯定報錯
是以Closure雖然很友善但是它一定會和使用它的上下文有極強的關聯。要不作為類似回調這樣的東西我如何知道調用者傳遞什麼參數給Closure呢
此問題如何破解隻能通過查詢API文檔才能了解上下文語義。比如下圖8
圖8 文檔說明
圖8中
each函數說明中将給指定的closure傳遞Set中的每一個item。是以closure的參數隻有一個。
findAll中絕對抓瞎了。一個是沒說明往Closure裡傳什麼。另外沒說明Closure的傳回值是什麼.....。
對Map的findAll而言Closure可以有兩個參數。findAll會将Key和Value分别傳進去。并且Closure傳回true表示該元素是自己想要的。傳回false表示該元素不是自己要找的。示意代碼如圖9所示
圖9 Closure調用示例
Closure的使用有點坑很大程度上依賴于你對API的熟悉程度是以最初階段SDK查詢是少不了的。
最後我們來看一下Groovy中比較進階的用法。
Groovy中可以像Java那樣寫package然後寫類。比如在檔案夾com/cmbc/groovy/目錄中放一個檔案叫Test.groovy如圖10所示
圖10 com/cmbc/groovy/Test.groovy檔案
你看圖10中的Test.groovy和Java類就很相似了。當然如果不聲明public/private等通路權限的話Groovy中類及其變量預設都是public的。
現在我們在測試的根目錄下建立一個test.groovy檔案。其代碼如下所示
圖11 test.groovy通路com/cmbc/groovy包
你看test.groovy先import了com.cmbc.groovy.Test類然後建立了一個Test類型的對象接着調用它的print函數。
這兩個groovy檔案的目錄結構如圖12所示
圖12 Test.groovy和test.groovy目錄結構
在groovy中系統自帶會加載目前目錄/子目錄下的xxx.groovy檔案。是以當執行groovy test.groovy的時候test.groovy import的Test類能被自動搜尋并加載到。
Java中我們最熟悉的是類。但是我們在Java的一個源碼檔案中不能不寫classinterface或者其他....而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的編譯指令-dclasses用于将編譯得到的class檔案拷貝到classes檔案夾下
圖13是test.groovy腳本轉換得到的java class。用jd-gui反編譯它的代碼
圖13 groovy腳本反編譯得到的Java類源碼
圖13中
test.groovy被轉換成了一個test類它從script派生。
每一個腳本都會生成一個static main函數。這樣當我們groovytest.groovy的時候其實就是用java去執行這個main函數
腳本中的所有代碼都會放到run函數中。比如println 'Groovy world'這句代碼實際上是包含在run函數裡的。
如果腳本中定義了函數則函數會被定義在test類中。
groovyc是一個比較好的指令讀者要掌握它的用法。然後利用jd-gui來檢視對應class的Java源碼。
前面說了xxx.groovy隻要不是和Java那樣的class那麼它就是一個腳本。而且腳本的代碼其實都會被放到run函數中去執行。那麼在Groovy的腳本中很重要的一點就是腳本中定義的變量和它的作用域。舉例
def x = 1 <==注意這個x有def或者指明類型比如 int x = 1
def printx(){
println x
printx() <==報錯說x找不到
為什麼繼續來看反編譯後的class檔案。
圖14 反編譯後的test.class檔案
圖14中
l printx被定義成test類的成員函數
l def x = 1這句話是在run中建立的。是以x=1從代碼上看好像是在整個腳本中定義的但實際上printx通路不了它。printx是test成員函數除非x也被定義成test的成員函數否則printx不能通路它。
那麼如何使得printx能通路x呢很簡單定義的時候不要加類型和def。即
x = 1 <==注意去掉def或者類型
printx() <==OK
這次Java源碼又變成什麼樣了呢
圖15 進化版的test.groovy
圖15中x也沒有被定義成test的成員函數而是在run的執行過程中将x作為一個屬性添加到test執行個體對象中了。然後在printx中先擷取這個屬性。
注意Groovy的文檔說 x = 1這種定義将使得x變成test的成員變量但從反編譯情況看這是不對得.....
雖然printx可以通路x變量了但是假如有其他腳本卻無法通路x變量。因為它不是test的成員變量。
比如我在測試目錄下建立一個新的名為test1.groovy。這個test1将通路test.groovy中定義的printx函數
圖16 test1.groovy使用test.groovy中的函數
這種方法使得我們可以将代碼分成子產品來編寫比如将公共的功能放到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檔案得到
圖17 x現在是test類的成員變量了
這個時候test.groovy中的x就成了test類的成員函數了。如此我們可以在script中定義那些需要輸出給外部腳本或類使用的變量了
本節介紹下Groovy的檔案I/O操作。直接來看例子吧雖然比Java看起來簡單但要了解起來其實比較難。尤其是當你要自己查SDK并編寫代碼的時候。
整體說來Groovy的I/O操作是在原有Java I/O操作上進行了更為簡單友善的封裝并且使用Closure來簡化代碼編寫。主要封裝了如下一些了類
圖18 Groovy File I/o常用類和SDK文檔位置
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
和讀檔案差不多。不再啰嗦。這裡給個例子告訴大家如何copy檔案。
def srcFile = new File(源檔案名)
def targetFile = new File(目标檔案名)
targetFile.withOutputStream{ os->
srcFile.withInputStream{ ins->
os << ins //利用OutputStream的<<操作符重載完成從inputstream到OutputStream
//的輸出
}
尼瑪....關于OutputStream的<<操作符重載檢視SDK文檔後可知
圖19 OutputStream的<<操作符重載
再一次向極緻簡單緻敬。但是SDK恐怕是離不開手了...
除了I/O異常簡單之外Groovy中的XML操作也極緻得很。Groovy中XML的解析提供了和XPath類似的方法名為GPath。這是一個類提供相應API。關于XPath請腦補https://en.wikipedia.org/wiki/XPath。
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 available="5" id="4">
<title>Don Xijote</title>
<author id="4">Manuel De Cervantes</author>
</books>
</value>
</response>
l 現在來看怎麼玩轉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 '
擷取屬性更直覺
author.@id == '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 androidManifest.@'android:versionName'
作為一門語言Groovy是複雜的是需要深入學習和鑽研的。一本厚書甚至都無法描述Groovy的方方面面。
Anyway從使用角度看尤其是又限定在Gradle這個領域内能用到的都是Groovy中一些簡單的知識。
現在正式進入Gradle。Gradle是一個工具同時它也是一個程式設計架構。前面也提到過使用這個工具可以完成app的編譯打包等工作。當然你也可以用它幹其他的事情。
Gradle是什麼學習它到什麼地步就可以了
----------------------------------------------------------------------------------------------------------
=====>看待問題的時候所站的角度非常重要。
-->當你把Gradle當工具看的時候我們隻想着如何用好它。會寫、寫好配置腳本就OK
-->當你把它當做程式設計架構看的時候你可能需要學習很多更深入的内容。
另外今天我們把它當工具看明天因為需求發生變化我們可能又得把它當程式設計架構看。
Gradle的官網http://gradle.org/
文檔位置https://docs.gradle.org/current/release-notes。其中的UserGuide和DSL Reference很關鍵。User Guide就是介紹Gradle的一本書而DSLReference是Gradle
API的說明。
以Ubuntu為例下載下傳Gradlehttp://gradle.org/gradle-download/ 選擇Completedistribution和Binary only distribution都行。然後解壓到指定目錄。
最後設定~/.bashrc把Gradle加到PATH裡如圖20所示
圖20 配置Gradle到bashrc
執行source ~/.bashrc初始化環境。
執行gradle --version如果成功運作就OK了。
注意為什麼說Gradle是一個程式設計架構來看它提供的API文檔
https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html
圖21 Project接口說明
原來我們編寫所謂的編譯腳本其實就是玩Gradle的API....是以它從更底層意義上看是一個程式設計架構
既然是程式設計架構我在講解Gradle的時候盡量會從API的角度來介紹。有些讀者肯定會不耐煩為嘛這麼費事
從我個人的經曆來看因為我從網上學習到的資料來看幾乎全是從腳本的角度來介紹Gradle結果學習一通下來隻記住參數怎麼配置卻不知道它們都是函數調用都是嚴格對應相關API的。
而從API角度來看待Gradle的話有了SDK文檔你就可以程式設計。程式設計是靠記住一行行代碼來實作的嗎不是是在你掌握大體流程然後根據SDK+API來完成的
其實Gradle自己的User Guide也明确說了
Buildscripts are code
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來定義和執行的。
下面我們來看一個實際的例子。這個例子非常有代表意義。圖22是一個名為posdevice的目錄。這個目錄裡包含3個Android Library工程2個Android APP工程。
圖22 重要例子
在圖22的例子中
l CPosDeviceSdk、CPosSystemSdk、CPosSystemSdkxxxImpl是Android Library。其中CPosSystemSdkxxxImpl依賴CPosSystemSdk
l 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但是要獨立編譯他們的話得
l cd 某個Project的目錄。比如 cd CPosDeviceSdk
l 然後執行 gradle xxxxxxx是任務的名字。對Android來說assemble這個Task會生成最終的産物是以gradleassemble
這很麻煩啊有10個獨立Project就得重複執行10次這樣的指令。更有甚者所謂的獨立Project其實有依賴關系的。比如我們這個例子。
那麼我想在posdevice目錄下直接執行gradle assemble是否能把這5個Project的東西都編譯出來呢
答案自然是可以。在Gradle中這叫Multi-Projects Build。把posdevice改造成支援Gradle的Multi-Projects Build很容易需要
l 在posdevice下也添加一個build.gradle。這個build.gradle一般幹得活是配置其他子Project的。比如為子Project添加一些屬性。這個build.gradle有沒有都無所屬。
l 在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加載的時候會執行initMinshengGradleEnvironment
initMinshengGradleEnvironment()
//include也是一個函數
include 'CPosSystemSdk' , 'CPosDeviceSdk' ,
'CPosSdkDemo','CPosDeviceServerApk','CPosSystemSdkWizarPosImpl'
到目前為止我們了解了Gradle什麼呢
l 每一個Project都必須設定一個build.gradle檔案。至于其内容我們留到後面再說。
l 對于multi-projects build需要在根目錄下也放一個build.gradle和一個settings.gradle。
l 一個Project是由若幹tasks來組成的當gradlexxx的時候實際上是要求gradle執行xxx任務。這個任務就能完成具體的工作。
l 當然具體的工作和不同的插件有關系。編譯Java要使用Java插件編譯Android APP需要使用Android APP插件。這些我們都留待後續讨論
gradle提供一些友善指令來檢視和ProjectTask相關的資訊。比如在posdevice中我想看這個multi projects到底包含多少個子Project
執行gradle projects得到圖23
圖23 gradle projects
你看multi projects的情況下posdevice這個目錄對應的build.gradle叫Root
Project它包含5個子Project。
如果你修改settings.gradle使得include隻有一個參數則gradle projects的子project也會變少比如圖24
圖24 修改settings.gradle使得隻包含CPosSystemSdk工程
檢視了Project資訊這個還比較簡單直接看settings.gradle也知道。那麼Project包含哪些Task資訊怎麼看呢圖23,24中最後的輸出也告訴你了想看某個Project包含哪些Task資訊隻要執行
gradleproject-path:tasks 就行。注意project-path是目錄名後面必須跟冒号。
對于Multi-project在根目錄中需要指定你想看哪個poject的任務。不過你要是已經cd到某個Project的目錄了則不需指定Project-path。
來看圖25
圖25 gradle CPosSystemSdk:tasks
圖25是gradleCPosSystemSdk:tasks的結果。
cd CPossystemSdk
gradle tasks 得到同樣的結果
CPosSystemSdk是一個Android Library工程Android Library對應的插件定義了好多Task。每種插件定義的Task都不盡相同這就是所謂的Domain Specific需要我們對相關領域有比較多的了解。
這些都是後話我們以後會詳細介紹。
圖25中列出了好多任務這時候就可以通過 gradle 任務名來執行某個任務。這和make xxx很像。比如
l gradle clean是執行清理任務和make clean類似。
l gradle properites用來檢視所有屬性資訊。
gradle tasks會列出每個任務的描述通過描述我們大概能知道這些任務是幹什麼的.....。然後gradletask-name執行它就好。
這裡要強調一點Task和Task之間往往是有關系的這就是所謂的依賴關系。比如assemble task就依賴其他task先執行assemble才能完成最終的輸出。
依賴關系對我們使用gradle有什麼意義呢
如果知道Task之間的依賴關系那麼開發者就可以添加一些定制化的Task。比如我為assemble添加一個SpecialTest任務并指定assemble依賴于SpecialTest。當assemble執行的時候就會先處理完它依賴的task。自然SpecialTest就會得到執行了...
大家先了解這麼多等後面介紹如何寫gradle腳本的時候這就是調用幾個函數的事情Nothing Special!
Gradle的工作流程其實蠻簡單用一個圖26來表達
圖26 Gradle工作流程
圖26告訴我們Gradle工作包含三個階段
l 首先是初始化階段。對我們前面的multi-project build而言就是執行settings.gradle
l Initiliazation phase的下一個階段是Configration階段。
l Configration階段的目标是解析每個project中的build.gradle。比如multi-project build例子中解析每個子目錄中的build.gradle。在這兩個階段之間我們可以加一些定制化的Hook。這當然是通過API來添加的。
l Configuration階段完了後整個build的project以及内部的Task關系就确定了。恩前面說過一個Project包含很多Task每個Task之間有依賴關系。Configuration會建立一個有向圖來描述Task之間的依賴關系。是以我們可以添加一個HOOK即當Task關系圖建立好後執行一些操作。
l 最後一個階段就是執行任務了。當然任務執行完後我們還可以加Hook。
下面展示一下我按圖26為posdevice項目添加的Hook它的執行結果
圖26 加了Hook後的執行結果
我在
l settings.gradle加了一個輸出。
l 在posdevice的build.gradle加了圖25中的beforeProject函數。
l 在CPosSystemSdk加了taskGraph whenReady函數和buidFinished函數。
好了Hook的代碼怎麼寫估計你很好奇而且肯定會埋汰搞毛這麼就還沒告訴我怎麼寫Gradle。馬上了
最後關于Gradle的工作流程你隻要記住
l Gradle有一個初始化流程這個時候settings.gradle會執行。
l 在配置階段每個Project都會被解析其内部的任務也會被添加到一個有向圖裡用于解決執行過程中的依賴關系。
l 然後才是執行階段。你在gradle xxx中指定什麼任務gradle就會将這個xxx任務鍊上的所有任務全部按依賴順序執行一遍
下面來告訴你怎麼寫代碼
希望你在進入此節之前一定花時間把前面内容看一遍
https://docs.gradle.org/current/dsl/ <==這個文檔很重要
Gradle基于GroovyGroovy又基于Java。是以Gradle執行的時候和Groovy一樣會把腳本轉換成Java對象。Gradle主要有三種對象這三種對象和三種不同的腳本檔案對應在gradle執行的時候會将腳本轉換成對應的對端
l Gradle對象當我們執行gradle xxx或者什麼的時候gradle會從預設的配置腳本中構造出一個Gradle對象。在整個執行過程中隻有這麼一個對象。Gradle對象的資料類型就是Gradle。我們一般很少去定制這個預設的配置腳本。
l Project對象每一個build.gradle會轉換成一個Project對象。
l Settings對象顯然每一個settings.gradle都會轉換成一個Settings對象。
注意對于其他gradle檔案除非定義了class否則會轉換成一個實作了Script接口的對象。這一點和3.5節中Groovy的腳本類相似
當我們執行gradle的時候gradle首先是按順序解析各個gradle檔案。這裡邊就有所所謂的生命周期的問題即先解析誰後解析誰。圖27是Gradle文檔中對生命周期的介紹結合上一節的内容相信大家都能看明白了。現在隻需要看紅框裡的内容
圖27 Gradle對LifeCycle的介紹
我們先來看Gradle對象它有哪些屬性呢如圖28所示
圖28 Gradle的屬性
我在posdevice build.gradle中和settings.gradle中分别加了如下輸出
//在settings.gradle中則輸出"In settings,gradle id is"
println "In posdevice, gradle id is " +gradle.hashCode()
println "Home Dir:" + gradle.gradleHomeDir
println "User Home Dir:" + gradle.gradleUserHomeDir
println "Parent: " + gradle.parent
得到結果如圖29所示
圖29 gradle示例
l 你看在settings.gradle和posdevice build.gradle中我們得到的gradle執行個體對象的hashCode是一樣的都是791279786。
l HomeDir是我在哪個目錄存儲的gradle可執行程式。
l User Home Dir是gradle自己設定的目錄裡邊存儲了一些配置檔案以及編譯過程中的緩存檔案生成的類檔案編譯中依賴的插件等等。
Gradle的函數接口在文檔中也有。
每一個build.gradle檔案都會轉換成一個Project對象。在Gradle術語中Project對象對應的是BuildScript。
Project包含若幹Tasks。另外由于Project對應具體的工程是以需要為Project加載所需要的插件比如為Java工程加載Java插件。其實一個Project包含多少Task往往是插件決定的。
是以在Project中我們要
l 加載插件。
l 不同插件有不同的行話即不同的配置。我們要在Project中配置好這樣插件就知道從哪裡讀取源檔案等
l 設定屬性。
Project的API位于https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html。加載插件是調用它的apply函數.apply其實是Project實作的PluginAware接口定義的
圖30 apply函數
來看代碼
[apply函數的用法]
apply是一個函數此處調用的是圖30中最後一個apply函數。注意Groovy支援
函數調用的時候通過 參數名1:參數值2參數名2參數值2 的方式來傳遞參數
apply plugin: 'com.android.library' <==如果是編譯Library則加載此插件
apply plugin: 'com.android.application' <==如果是編譯Android APP則加載此插件
除了加載二進制的插件上面的插件其實都是下載下傳了對應的jar包這也是通常意義上我們所了解的插件還可以加載一個gradle檔案。為什麼要加載gradle檔案呢
其實這和代碼的子產品劃分有關。一般而言我會把一些通用的函數放到一個名叫utils.gradle檔案裡。然後在其他工程的build.gradle來加載這個utils.gradle。這樣通過一些處理我就可以調用utils.gradle中定義的函數了。
加載utils.gradle插件的代碼如下
utils.gradle是我封裝的一個gradle腳本裡邊定義了一些友善函數比如讀取AndroidManifest.xml中
的versionName或者是copy jar包/APK包到指定的目錄
apply from: rootProject.getRootDir().getAbsolutePath() + "/utils.gradle"
也是使用apply的最後一個函數。那麼apply最後一個函數到底支援哪些參數呢還是得看圖31中的API說明
圖31 apply API說明
我這裡不遺餘力的列出API圖檔就是希望大家在寫腳本的時候碰到不會的一定要去檢視API文檔
如果是單個腳本則不需要考慮屬性的跨腳本傳播但是Gradle往往包含不止一個build.gradle檔案比如我設定的utils.gradlesettings.gradle。如何在多個腳本中設定屬性呢
Gradle提供了一種名為extra property的方法。extra property是額外屬性的意思在第一次定義該屬性的時候需要通過ext字首來标示它是一個額外的屬性。定義好之後後面的存取就不需要ext字首了。ext屬性支援Project和Gradle對象。即Project和Gradle對象都可以設定ext屬性
舉個例子
我在settings.gradle中想為Gradle對象設定一些外置屬性是以在initMinshengGradleEnvironment函數中
//屬性值從local.properites中讀取
Propertiesproperties = new Properties()
FilepropertyFile = new File(rootDir.getAbsolutePath() +"/local.properties")
properties.load(propertyFile.newDataInputStream())
//gradle就是gradle對象。它預設是Settings和Project的成員變量。可直接擷取
//ext字首表明操作的是外置屬性。api是一個新的屬性名。前面說過隻在
//第一次定義或者設定它的時候需要ext字首
gradle.ext.api =properties.getProperty('sdk.api')
println gradle.api //再次存取api的時候就不需要ext字首了
......
}
再來一個例子強化一下
我在utils.gradle中定義了一些函數然後想在其他build.gradle中調用這些函數。那該怎麼做呢
[utils.gradle]
//utils.gradle中定義了一個擷取AndroidManifests.xmlversionName的函數
def getVersionNameAdvanced(){
下面這行代碼中的project是誰
defxmlFile = project.file("AndroidManifest.xml")
defrootManifest = new XmlSlurper().parse(xmlFile)
returnrootManifest['@android:versionName']
//現在想把這個API輸出到各個Project。由于這個utils.gradle會被每一個Project Apply是以
//我可以把getVersionNameAdvanced定義成一個closure然後指派到一個外部屬性
下面的ext是誰的ext
ext{ //此段花括号中代碼是閉包
//除了ext.xxx=value這種定義方法外還可以使用ext{}這種書寫方法。
//ext{}不是ext(Closure)對應的函數調用。但是ext{}中的{}确實是閉包。
getVersionNameAdvanced = this.&getVersionNameAdvanced
}
上面代碼中有兩個問題
project是誰
ext是誰的ext
上面兩個問題比較關鍵我也是花了很長時間才搞清楚。這兩個問題歸結到一起其實就是
加載utils.gradle的Project對象和utils.gradle本身所代表的Script對象到底有什麼關系
我們在Groovy中也講過怎麼在一個Script中import另外一個Script中定義的類或者函數見3.5 腳本類、檔案I/O和XML操作一節。在Gradle中這一塊的處理比Groovy要複雜具體怎麼搞我還沒完全弄清楚但是Project和utils.gradle對于的Script的對象的關系是
l 當一個Project apply一個gradle檔案的時候這個gradle檔案會轉換成一個Script對象。這個相信大家都已經知道了。
l Script中有一個delegate對象這個delegate預設是加載即調用apply它的Project對象。但是在apply函數中有一個from參數還有一個to參數參考圖31。通過to參數你可以把delegate對象指定為别的東西。
l delegate對象是什麼意思當你在Script中操作一些不是Script自己定義的變量或者函數時候gradle會到Script的delegate對象去找看看有沒有定義這些變量或函數。
現在你知道問題1,2和答案了
問題1project就是加載utils.gradle的project。由于posdevice有5個project是以utils.gradle會分别加載到5個project中。是以getVersionNameAdvanced才不用區分到底是哪個project。反正一個project有一個utils.gradle對應的Script。
問題2ext自然就是Project對應的ext了。此處為Project添加了一些closure。那麼在Project中就可以調用getVersionNameAdvanced函數了
比如我在posdevice每個build.gradle中都有如下的代碼
tasks.getByName("assemble"){
it.doLast{
println "$project.name: After assemble, jar libs are copied tolocal repository"
copyOutput(true) //copyOutput是utils.gradle輸出的closure
}
通過這種方式我将一些常用的函數放到utils.gradle中然後為加載它的Project設定ext屬性。最後Project中就可以調用這種指派函數了
注意此處我研究的還不是很深而且我個人感覺
1 在Java和Groovy中我們會把常用的函數放到一個輔助類和公共類中然後在别的地方import并調用它們。
2 但是在Gradle更正規的方法是在xxx.gradle中定義插件。然後通過添加Task的方式來完成工作。gradle的user guide有詳細介紹如何實作自己的插件。
Task是Gradle中的一種資料類型它代表了一些要執行或者要幹的工作。不同的插件可以添加不同的Task。每一個Task都需要和一個Project關聯。
Task的API文檔位于https://docs.gradle.org/current/dsl/org.gradle.api.Task.html。關于Task我這裡簡單介紹下build.gradle中怎麼寫它以及Task中一些常見的類型
關于Task。來看下面的例子
[build.gradle]
/
/Task是和Project關聯的是以我們要利用Project的task函數來建立一個Task
task myTask <==myTask是建立Task的名字
task myTask { configure closure }
task myType << { task action } <==注意<<符号是doLast的縮寫
task myTask(type: SomeType)
task myTask(type: SomeType) { configure closure }
上述代碼中都用了Project的一個函數名為task注意
l 一個Task包含若幹Action。是以Task有doFirst和doLast兩個函數用于添加需要最先執行的Action和需要和需要最後執行的Action。Action就是一個閉包。
l Task建立的時候可以指定Type通過type:名字表達。這是什麼意思呢其實就是告訴Gradle這個建立的Task對象會從哪個基類Task派生。比如Gradle本身提供了一些通用的Task最常見的有Copy 任務。Copy是Gradle中的一個類。當我們task myTask(type:Copy)的時候建立的Task就是一個Copy Task。
l 當我們使用 taskmyTask{ xxx}的時候。花括号是一個closure。這會導緻gradle在建立這個Task之後傳回給使用者之前會先執行closure的内容。
l 當我們使用taskmyTask << {xxx}的時候我們建立了一個Task對象同時把closure做為一個action加到這個Task的action隊列中并且告訴它“最後才執行這個closure”注意<<符号是doLast的代表。
圖32是Project中關于task函數說明
圖32 Project中task函數
陸陸續續講了這麼些内容我自己感覺都有點煩了。是得Gradle用一整本書來講都嫌不夠呢。
anyway到目前為止我介紹的都是一些比較基礎的東西還不是特别多。但是後續例子該涉及到的知識點都有了。下面我們直接上例子。這裡有兩個例子
l posdevice的例子
l 另外一個是單個project的例子
現在正是開始通過例子來介紹怎麼玩gradle。這裡要特别強調一點根據Gradle的哲學。gradle檔案中包含一些所謂的Script Block姑且這麼稱它。ScriptBlock作用是讓我們來配置相關的資訊。不同的SB有不同的需要配置的東西。這也是我最早說的行話。比如源碼對應的SB就需要我們配置源碼在哪個檔案夾裡。關于SB我們後面将見識到
posdevice是一個multi project。下面包含5個Project。對于這種Project請大家回想下我們該建立哪些檔案
l settings.gradle是必不可少的
l 根目錄下的build.gradle。這個我們沒講過因為posdevice的根目錄本身不包含代碼而是包含其他5個子project。
l 每個project目錄下包含對于的build.gradle
l 另外我把常用的函數封裝到一個名為utils.gradle的腳本裡了。
馬上一個一個來看它們。
utils.gradle是我自己加的為我們團隊特意加了一些常見函數。主要代碼如下
import groovy.util.XmlSlurper //解析XML時候要引入這個groovy的package
def copyFile(String srcFile,dstFile){
......//拷貝檔案函數用于将最後的生成物拷貝到指定的目錄
def rmFile(String targetFile){
.....//删除指定目錄中的檔案
def cleanOutput(boolean bJar = true){
....//clean的時候清理
def copyOutput(boolean bJar = true){
....//copyOutput内部會調用copyFile完成一次build的産出物拷貝
def getVersionNameAdvanced(){//老朋友
//對于android library編譯我會disable所有的debug編譯任務
def disableDebugBuild(){
//project.tasks包含了所有的tasks下面的findAll是尋找那些名字中帶debug的Task。
//傳回值儲存到targetTasks容器中
def targetTasks = project.tasks.findAll{task ->
task.name.contains("Debug")
}
//對滿足條件的task設定它為disable。如此這般這個Task就不會被執行
targetTasks.each{
println"disable debug task :${it.name}"
it.setEnabled false
//将函數設定為extra屬性中去這樣加載utils.gradle的Project就能調用此檔案中定義的函數了
ext{
copyFile= this.&copyFile
rmFile =this.&rmFile
cleanOutput = this.&cleanOutput
copyOutput = this.&copyOutput
getVersionNameAdvanced = this.&getVersionNameAdvanced
disableDebugBuild = this.&disableDebugBuild
圖33展示了被disable的Debug任務的部分資訊
圖33 disable的Debug Task資訊
這個檔案中我們該幹什麼調用include把需要包含的子Project加進來。代碼如下
/*我們團隊内部建立的編譯環境初始化函數
這個函數的目的是
1 解析一個名為local.properties的檔案讀取AndroidSDK和NDK的路徑
2 擷取最終産出物目錄的路徑。這樣編譯完的apk或者jar包将拷貝到這個最終産出物目錄中
3 擷取Android SDK指定編譯的版本
*/
Properties properties = new Properties()
//local.properites也放在posdevice目錄下
FilepropertyFile = new File(rootDir.getAbsolutePath()+ "/local.properties")
/*
根據Project、Gradle生命周期的介紹settings對象的建立位于具體Project建立之前
而Gradle底對象已經建立好了。是以我們把local.properties的資訊讀出來後通過
extra屬性的方式設定到gradle對象中
而具體Project在執行的時候就可以直接從gradle對象中得到這些屬性了
*/
gradle.ext.sdkDir =properties.getProperty('sdk.dir')
gradle.ext.ndkDir =properties.getProperty('ndk.dir')
gradle.ext.localDir =properties.getProperty('local.dir')
//指定debugkeystore檔案的位置debug版apk簽名的時候會用到
gradle.ext.debugKeystore= properties.getProperty('debug.keystore')
......
//初始化
//添加子Project資訊
include 'CPosSystemSdk' , 'CPosDeviceSdk' ,'CPosSdkDemo','CPosDeviceServerApk', 'CPosSystemSdkWizarPosImpl'
注意對于Android來說local.properties檔案是必須的它的内容如下
[local.properties]
local.dir=/home/innost/workspace/minsheng-flat-dir/
//注意根據Android Gradle的規範隻有下面兩個屬性是必須的其餘都是我自己加的
sdk.dir=/home/innost/workspace/android-aosp-sdk/
ndk.dir=/home/innost/workspace/android-aosp-ndk/
debug.keystore=/home/innost/workspace/tools/mykeystore.jks
sdk.api=android-19
再次強調sdk.dir和ndk.dir是Android Gradle必須要指定的其他都是我自己加的屬性。當然。不編譯ndk就不需要ndk.dir屬性了。
作為multi-project根目錄一般情況下它的build.gradle是做一些全局配置。來看我的build.gradle
[posdevicebuild.gradle]
//下面這個subprojects{}就是一個Script Block
subprojects {
println"Configure for $project.name" //周遊子Projectproject變量對應每個子Project
buildscript { //這也是一個SB
repositories {//repositories是一個SB
///jcenter是一個函數表示編譯過程中依賴的庫所需的插件可以在jcenter倉庫中
//下載下傳。
jcenter()
dependencies { //SB
//dependencies表示我們編譯的時候依賴android開發的gradle插件。插件對應的
//class path是com.android.tools.build。版本是1.2.3
classpath'com.android.tools.build:gradle:1.2.3'
//為每個子Project加載utils.gradle 。當然這句話可以放到buildscript花括号之後
applyfrom: rootProject.getRootDir().getAbsolutePath() + "/utils.gradle"
}//buildscript結束
感覺解釋得好蒼白SB在Gradle的API文檔中也是有的。先來看Gradle定義了哪些SB。如圖34所示
圖34 SB的類型
你看subprojects、dependencies、repositories都是SB。那麼SB到底是什麼它是怎麼完成所謂配置的呢
仔細研究你會發現SB後面都需要跟一個花括号而花括号恩我們感覺裡邊可能一個Closure。由于圖34說這些SB的Description都有“Configure xxx for this project”是以很可能subprojects是一個函數然後其參數是一個Closure。是這樣的嗎
Absolutely right。隻是這些函數你直接到Project API裡不一定能找全。不過要是你好奇心重不妨到https://docs.gradle.org/current/javadoc/選擇Index這一項然後ctrl+f輸入圖34中任何一個Block你都會找到對應的函數。比如我替你找了幾個API如圖35所示
圖35 SB對應的函數
特别提示當你下次看到一個不認識的SB的時候就去看API吧。
下面來解釋代碼中的各個SB
l subprojects它會周遊posdevice中的每個子Project。在它的Closure中預設參數是子Project對應的Project對象。由于其他SB都在subprojects花括号中是以相當于對每個Project都配置了一些資訊。
l buildscript它的closure是在一個類型為ScriptHandler的對象上執行的。主意用來所依賴的classpath等資訊。通過檢視ScriptHandler API可知在buildscript SB中你可以調用ScriptHandler提供的repositories(Closure )、dependencies(Closure)函數。這也是為什麼repositories和dependencies兩個SB為什麼要放在buildscript的花括号中的原因。明白了這就是所謂的行話得知道規矩。不知道規矩你就亂了。記不住規矩又不知道查SDK那麼就徹底抓瞎隻能到網上到處找答案了
l 關于repositories和dependencies大家直接看API吧。後面碰到了具體代碼我們再來介紹
CPosDeviceSdk是一個Android Library。按Google的想法Android Library編譯出來的應該是一個AAR檔案。但是我的項目有些特殊我需要釋出CPosDeviceSdk.jar包給其他人使用。jar在編譯過程中會生成但是它不屬于Android Library的标準輸出。在這種情況下我需要在編譯完成後主動copy jar包到我自己設計的産出物目錄中。
//Library工程必須加載此插件。注意加載了Android插件就不要加載Java插件了。因為Android
//插件本身就是拓展了Java插件
apply plugin: 'com.android.library'
//android的編譯增加了一種新類型的ScriptBlock-->android
android {
//你看我在local.properties中設定的API版本号就可以一次設定多個Project使用了
//借助我特意設計的gradle.ext.api屬性
compileSdkVersion =gradle.api //這兩個紅色的參數必須設定
buildToolsVersion = "22.0.1"
sourceSets{ //配置源碼路徑。這個sourceSets是Java插件引入的
main{ //mainAndroid也用了
manifest.srcFile 'AndroidManifest.xml' //這是一個函數設定manifest.srcFile
aidl.srcDirs=['src'] //設定aidl檔案的目錄
java.srcDirs=['src'] //設定java檔案的目錄
}
dependencies { //配置依賴關系
//compile表示編譯和運作時候需要的jar包fileTree是一個函數
//dir:'libs'表示搜尋目錄的名稱是libs。include:['*.jar']表示搜尋目錄下滿足*.jar名字的jar
//包都作為依賴jar檔案
compile fileTree(dir: 'libs', include: ['*.jar'])
} //android SB配置完了
//clean是一個Task的名字這個Task好像是Java插件這裡是Android插件引入的。
//dependsOn是一個函數下面這句話的意思是 clean任務依賴cposCleanTask任務。是以
//當你gradle clean以執行clean Task的時候cposCleanTask也會執行
clean.dependsOn 'cposCleanTask'
//建立一個Task
task cposCleanTask() <<{
cleanOutput(true) //cleanOutput是utils.gradle中通過extra屬性設定的Closure
//前面說了我要把jar包拷貝到指定的目錄。對于Android編譯我一般指定gradle assemble
//它預設編譯debug和release兩種輸出。是以下面這個段代碼表示
//tasks代表一個Projects中的所有Task是一個容器。getByName表示找到指定名稱的任務。
//我這裡要找的assemble任務然後我通過doLast添加了一個Action。這個Action就是copy
//産出物到我設定的目标目錄中去
copyOutput(true)
/*
因為我的項目隻提供最終的release編譯出來的Jar包給其他人是以不需要編譯debug版的東西
當Project建立完所有任務的有向圖後我通過afterEvaluate函數設定一個回調Closure。在這個回調
Closure裡我disable了所有Debug的Task
project.afterEvaluate{
disableDebugBuild()
Android自己定義了好多ScriptBlock。Android定義的DSL參考文檔在
https://developer.android.com/tools/building/plugin-for-gradle.html下載下傳。注意它居然沒有提供線上文檔。
圖36所示為Android的DSL參考資訊。
圖36 Android Gradle DSL參考示意
圖37為buildToolsVersion和compileSdkVersion的說明
圖37 buildToolsVersion和compileSdkVersion的說明
從圖37可知這兩個變量是必須要設定的.....
再來看一個APK的build它包含NDK的編譯并且還要簽名。根據項目的需求我們隻能簽debug版的而release版的簽名得釋出unsigned包給上司簽名。另外CPosDeviceServerAPK依賴CPosDeviceSdk。
雖然我可以先編譯CPosDeviceSdk得到對應的jar包然後設定CPosDeviceServerApk直接依賴這個jar包就好。但是我更希望CPosDeviceServerApk能直接依賴于CPosDeviceSdk這個工程。這樣整個posdevice可以做到這幾個Project的依賴關系是最新的。
apply plugin: 'com.android.application' //APK編譯必須加載這個插件
compileSdkVersion gradle.api
buildToolsVersion "22.0.1"
sourceSets{ //差不多的設定
main{
manifest.srcFile 'AndroidManifest.xml'
//通過設定jni目錄為空我們可不使用apk插件的jni編譯功能。為什麼因為據說
//APK插件的jni功能好像不是很好使....暈菜
jni.srcDirs = []
jniLibs.srcDir 'libs'
aidl.srcDirs=['src']
java.srcDirs=['src']
res.srcDirs=['res']
}//main結束
signingConfigs { //設定簽名資訊配置
debug { //如果我們在local.properties設定使用特殊的keystore則使用它
//下面這些設定無非是函數調用....請務必閱讀API文檔
if(project.gradle.debugKeystore != null){
storeFile file("file://${project.gradle.debugKeystore}")
storePassword "android"
keyAlias "androiddebugkey"
keyPassword "android"
}
}//signingConfigs結束
buildTypes {
debug {
signingConfig signingConfigs.debug
jniDebuggable false
}//buildTypes結束
dependencies {
//compileproject函數可指定依賴multi-project中的某個子project
compile project(':CPosDeviceSdk')
} //dependices結束
repositories{
flatDir {//flatDir告訴gradle編譯中依賴的jar包存儲在dirs指定的目錄
name "minsheng-gradle-local-repository"
dirsgradle.LOCAL_JAR_OUT //LOCAL_JAR_OUT是我存放編譯出來的jar包的位置
}//repositories結束
}//android結束
建立一個Task類型是Exec這表明它會執行一個指令。我這裡讓他執行ndk的
ndk-build指令用于編譯ndk。關于Exec類型的Task請自行腦補Gradle的API
//注意此處建立task的方法是直接{}喔那麼它後面的tasks.withType(JavaCompile)
//設定的依賴關系還有意義嗎Think如果你能想明白gradle掌握也就差不多了
task buildNative(type: Exec, description: 'CompileJNI source via NDK') {
if(project.gradle.ndkDir == null) //看看有沒有指定ndk.dir路徑
println "CANNOT Build NDK"
else{
commandLine "/${project.gradle.ndkDir}/ndk-build",
'-C', file('jni').absolutePath,
'-j', Runtime.runtime.availableProcessors(),
'all', 'NDK_DEBUG=0'
tasks.withType(JavaCompile) {
compileTask -> compileTask.dependsOn buildNative
......
//對于APK除了拷貝APK檔案到指定目錄外我還特意為它們加上了自動版本命名的功能
tasks.getByName("assemble"){
it.doLast{
project.ext.versionName = android.defaultConfig.versionName
println "\t versionName = $versionName"
copyOutput(false)
在posdevice下執行gradle assemble指令最終的輸出檔案都會拷貝到我指定的目錄結果如圖38所示
圖38 posdevice執行結果
圖38所示為posdevice gradle assemble的執行結果
l library包都編譯release版的copy到xxx/javaLib目錄下
l apk編譯debug和release-unsigned版的copy到apps目錄下
l 所有産出物都自動從AndroidManifest.xml中提取versionName。
下面這個執行個體也是來自一個實際的APP。這個APP對應的是一個單獨的Project。但是根據我前面的建議我會把它改造成支援Multi-ProjectsBuild的樣子。即在工程目錄下放一個settings.build。
另外這個app有一個特點。它有三個版本分别是debug、release和demo。這三個版本對應的代碼都完全一樣但是在運作的時候需要從assets/runtime_config檔案中讀取參數。參數不同則運作的時候會跳轉到debug、release或者demo的邏輯上。
注意我知道assets/runtime_config這種做法不decent但這是一個既有項目我們隻能做小範圍的适配而不是傷筋動骨改用更好的方法。另外從未來的需求來看暫時也沒有大改的必要。
引入gradle後我們該如何處理呢
解決方法是在編譯build、release和demo版本前在build.gradle中自動設定runtime_config的内容。代碼如下所示
apply plugin: 'com.android.application' //加載APP插件
//加載utils.gradle
apply from:rootProject.getRootDir().getAbsolutePath() + "/utils.gradle"
//buildscript設定android app插件的位置
buildscript {
repositories { jcenter() }
dependencies { classpath 'com.android.tools.build:gradle:1.2.3' }
//androidScriptBlock
compileSdkVersion gradle.api
buildToolsVersion "22.0.1"
sourceSets{//源碼設定SB
main{
jni.srcDirs = []
aidl.srcDirs=['src']
assets.srcDirs = ['assets'] //多了一個assets目錄
signingConfigs {//簽名設定
debug { //debug對應的SB。注意
storeFile file("file://${project.gradle.debugKeystore}")
storePassword "android"
keyAlias "androiddebugkey"
keyPassword "android"
最關鍵的内容來了 buildTypesScriptBlock.
buildTypes和上面的signingConfigs當我們在build.gradle中通過{}配置它的時候
其背後的所代表的對象是NamedDomainObjectContainer<BuildType> 和
NamedDomainObjectContainer<SigningConfig>
注意NamedDomainObjectContainer<BuildType/或者SigningConfig>是一種容器
容器的元素是BuildType或者SigningConfig。我們在debug{}要填充BuildType或者
SigningConfig所包的元素比如storePassword就是SigningConfig類的成員。而proguardFile等
是BuildType的成員。
那麼為什麼要使用NamedDomainObjectContainer這種資料結構呢因為往這種容器裡
添加元素可以采用這樣的方法 比如signingConfig為例
signingConfig{//這是一個NamedDomainObjectContainer<SigningConfig>
test1{//建立一個名為test1的SigningConfig元素然後添加到容器裡
//在這個花括号中設定SigningConfig的成員變量的值
}
test2{//建立一個名為test2的SigningConfig元素然後添加到容器裡
}
}
在buildTypes中Android預設為這幾個NamedDomainObjectContainer添加了
debug和release對應的對象。如果我們再添加别的名字的東西那麼gradleassemble的時候
也會編譯這個名字的apk出來。比如我添加一個名為test的buildTypes那麼gradle assemble
就會編譯一個xxx-test-yy.apk。在此test就好像debug、release一樣。
*/
buildTypes{
debug{ //修改debug的signingConfig為signingConfig.debug配置
demo{ //demo版需要混淆
proguardFile 'proguard-project.txt'
//release版沒有設定是以預設沒有簽名沒有混淆
......//其他和posdevice 類似的處理。來看如何動态生成runtime_config檔案
def runtime_config_file = 'assets/runtime_config'
/*
我們在gradle解析完整個任務之後找到對應的Task然後在裡邊添加一個doFirst Action
這樣能確定編譯開始的時候我們就把runtime_config檔案準備好了。
注意必須在afterEvaluate裡邊才能做否則gradle沒有建立完任務有向圖你是找不到
什麼preDebugBuild之類的任務的
project.afterEvaluate{
//找到preDebugBuild任務然後添加一個Action
tasks.getByName("preDebugBuild"){
it.doFirst{
println "generate debug configuration for ${project.name}"
def configFile = new File(runtime_config_file)
configFile.withOutputStream{os->
os << I am Debug\n' //往配置檔案裡寫 I am Debug
}
//找到preReleaseBuild任務
tasks.getByName("preReleaseBuild"){
println "generate release configuration for ${project.name}"
os << I am release\n'
}
//找到preDemoBuild。這個任務明顯是因為我們在buildType裡添加了一個demo的元素
//是以Android APP插件自動為我們生成的
tasks.getByName("preDemoBuild"){
println "generate offlinedemo configuration for${project.name}"
os << I am Demo\n'
}
.....//copyOutput
最終的結果如圖39所示
圖39 執行個體2的結果
幾個問題為什麼我知道有preXXXBuild這樣的任務
答案gradle tasks --all檢視所有任務。然後多嘗試幾次直到成功
到此我個人覺得Gradle相關的内容都講完了。很難相信我僅花了1個小時不到的時間就為執行個體2添加了gradle編譯支援。在一周以前我還覺得這是個心病。回想學習gradle的一個月時間裡走過不少彎路求解問題的思路也和最開始不一樣
l 最開始的時候我一直把gradle當做腳本看。然後到處到網上找怎麼配置gradle。可能能編譯成功但是完全不知道為什麼。比如NameDomainObjectContainer為什麼有debug、release。能自己加别的嗎不知道怎麼加沒有章法沒有參考。出了問題隻能google找到一個解法試一試成功就不管。這麼搞心裡不踏實。
l 另外對文法不熟悉尤其是Groovy文法雖然看了下快速教材但總感覺一到gradle就看不懂。主要問題還是閉包比如Groovy那一節寫得檔案拷貝的例子中的withOutputStream還有gradle中的withType都是些啥玩意啊
l 是以後來下決心先把Groovy學會主要是把自己暴露在閉包裡邊。另外Groovy是一門語言總得有SDK說明吧。寫了幾個例子慢慢體會到Groovy的好處也熟悉Groovy的文法了。
l 接着開始看Gradle。Gradle有幾本書我看過Gradle in Action。說實話看得非常痛苦。現在想起來Gradle其實比較簡單知道它的生命周期知道它怎麼解析腳本知道它的API幾乎很快就能幹活。而Gradle In Action一上來就很細而且沒有從API角度介紹。說個很有趣的事情書中有個類似下面的例子
task myTask << {
println 'I am myTask'
書中說如果代碼沒有加<<則這個任務在腳本initialization也就是你無論執行什麼任務這個任務都會被執行I am myTask都會被輸出的時候執行如果加了<<則在gradle myTask後才執行。
尼瑪我開始完全不知道為什麼死記硬背。現在你明白了嗎
這和我們調用task這個函數的方式有關如果沒有<<則閉包在task函數傳回前會執行而如果加了<<則變成調用myTask.doLast添加一個Action了自然它會等到grdle myTask的時候才會執行
現在想起這個事情我還是很憤怒API都說很清楚了......而且如果你把Gradle當做程式設計架構來看對于我們這些程式員來說寫這幾百行代碼那還算是事嘛