1. 程式人生 > >Gradle史上最詳細解析

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,但是要獨立編譯他們的話,得:

  1. cd  某個Project的目錄。比如 cd CPosDeviceSdk
  2. 然後執行 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