gradle深度好文(轉)

深入理解Android之Gradle

格式更加精美的PDF版請到:https://pan.baidu.com/s/1GfN6F8sOaKFAdz5y1bn3VQ下載

weibo分享失效,請各位到百度雲盤下載

 

Gradle是當前非常“勁爆”得構建工具。本篇文章就是專爲講解Gradle而來。介紹Gradle之前,先說點題外話。

一、題外話

說實話,我在大法工作的時候,就見過Gradle。但是當時我一直不知道這是什麼東西。而且大法工具組的工程師還將其和Android Studio大法版一起推送,偶一看就更沒興趣了。爲什麼那個時候如此不待見Gradle呢?因爲我此前一直是做ROM開發。在這個層面上,我們用make,mm或者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選擇了GroovyGroovy基於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介紹

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之間的關係。

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905192824392?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖1&nbsp; Java、Groovy和JVM的關係</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

實際上,由於Groovy Code在真正執行的時候已經變成了Java字節碼,所以JVM根本不知道自己運行的是Groovy代碼

 

下面我們將介紹Groovy。由於此文的主要目的是Gradle,所以我們不會過多討論Groovy中細枝末節的東西,而是把知識點集中在以後和Gradle打交道時一些常用的地方上。

3.1  Groovy開發環境

在學習本節的時候,最好部署一下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是安裝時候的示意圖

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905192939249?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖1&nbsp; Groovy安裝示意圖</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

然後,創建一個test.groovy文件,裏邊只有一行代碼:

 

 

println "hello groovy"

 

 

 

 

 

執行groovy test.groovy,輸出結果如圖2所示:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905192952248?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖2&nbsp; 執行groovy腳本</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

親們,必須要完成上面的操作啊。做完後,有什麼感覺和體會?

 

最大的感覺可能就是groovy和shell腳本,或者python好類似。

另外,除了可以直接使用JDK之外,Groovy還有一套GDK,網址是http://www.groovy-lang.org/api.html

說實話,看了這麼多家API文檔,還是Google的Android API文檔做得好。其頁面中右上角有一個搜索欄,在裏邊輸入一些關鍵字,瞬間就能列出候選類,相關文檔,方便得不得了啊.....

3.2  一些前提知識

爲了後面講述方面,這裏先介紹一些前提知識。初期接觸可能有些彆扭,看習慣就好了。

 

  1. l Groovy註釋標記和Java一樣,支持//或者/**/
  2. l Groovy語句可以不用分號結尾。Groovy爲了儘量減少代碼的輸入,確實煞費苦心
  3. l Groovy中支持動態類型,即定義變量的時候可以不指定其類型。Groovy中,變量定義可以使用關鍵字def。注意,雖然def不是必須的,但是爲了代碼清晰,建議還是使用def關鍵字
  4. def variable1 = 1 //可以不使用分號結尾
  5. def varable2 = "I ama person"
  6. def int x = 1 //變量定義時,也可以直接指定類型
  7. l 函數定義時,參數的類型也可以不指定。比如
  8. String testFunction(arg1,arg2){//無需指定參數類型
  9. ...
  10. }
  11. l 除了變量定義可以不指定類型外,Groovy中函數的返回值也可以是無類型的。比如:
  12. //無類型的函數定義,必須使用def關鍵字
  13. def nonReturnTypeFunc(){
  14. last_line //最後一行代碼的執行結果就是本函數的返回值
  15. }
  16. //如果指定了函數返回類型,則可不必加def關鍵字來定義函數
  17. String getString(){
  18. return"I am a string"
  19. }

 

 

 

 

 

其實,所謂的無返回類型的函數,我估計內部都是按返回Object類型來處理的。畢竟,Groovy是基於Java的,而且最終會轉成Java Code運行在JVM上

 

  1. l 函數返回值:Groovy的函數裏,可以不使用returnxxx來設置xxx爲函數返回值。如果不使用return語句的話,則函數裏最後一句代碼的執行結果被設置成返回值。比如
  2. //下面這個函數的返回值是字符串"getSomething return value"
  3. def getSomething(){
  4. "getSomething return value" //如果這是最後一行代碼,則返回類型爲String
  5. 1000//如果這是最後一行代碼,則返回類型爲Integer
  6. }

 

 

 

 

 

注意,如果函數定義時候指明瞭返回值類型的話,函數中則必須返回正確的數據類型,否則運行時報錯。如果使用了動態類型的話,你就可以返回任何類型了。

 

  1. l Groovy對字符串支持相當強大,充分吸收了一些腳本語言的優點:
  2. 1 單引號''中的內容嚴格對應Java中的String,不對$符號進行轉義
  3. defsingleQuote='I am $ dolloar' //輸出就是I am $ dolloar
  4. 2 雙引號""的內容則和腳本語言的處理有點像,如果字符中有$號的話,則它會$表達式先求值。
  5. defdoubleQuoteWithoutDollar = "I am one dollar" //輸出 I am one dollar
  6. def x = 1
  7. defdoubleQuoteWithDollar = "I am $x dolloar" //輸出I am 1 dolloar
  8. 3 三個引號'''xxx'''中的字符串支持隨意換行 比如
  9. defmultieLines = ''' begin
  10. line 1
  11. line 2
  12. end '''
  13. l 最後,除了每行代碼不用加分號外,Groovy中函數調用的時候還可以不加括號。比如:
  14. println("test") ---> println"test"
  15. 注意,雖然寫代碼的時候,對於函數調用可以不帶括號,但是Groovy經常把屬性和函數調用混淆。比如
  16. def getSomething(){
  17. "hello"
  18. }

 

 

 

 

 

getSomething()  //如果不加括號的話,Groovy會誤認爲getSomething是一個變量。比如:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905193011828?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖3&nbsp; 錯誤示意</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

所以,調用函數要不要帶括號,我個人意見是如果這個函數是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類

 

  1. 變量定義:List變量由[]定義,比如
  2. def aList = [5,'string',true] //List由[]定義,其元素可以是任何對象
  3. 變量存取:可以直接通過索引存取,而且不用擔心索引越界。如果索引超過當前鏈表長度,List會自動
  4. 往該索引添加元素
  5. assert aList[1] == 'string'
  6. assert aList[5] == null //第6個元素爲空
  7. aList[100] = 100 //設置第101個元素的值爲10
  8. assert aList[100] == 100
  9. 那麼,aList到現在爲止有多少個元素呢?
  10. println aList.size ===>結果是101

 

 

 

 

 

2.  Map類

 

  1. 容器變量定義
  2. 變量定義:Map變量由[:]定義,比如
  3. def aMap = ['key1':'value1','key2':true]
  4. Map由[:]定義,注意其中的冒號。冒號左邊是key,右邊是Value。key必須是字符串,value可以是任何對象。另外,key可以用''""包起來,也可以不用引號包起來。比如
  5. def aNewMap = [key1:"value",key2:true]//其中的key1和key2默認被
  6. 處理成字符串"key1""key2"
  7. 不過Key要是不使用引號包起來的話,也會帶來一定混淆,比如
  8. def key1="wowo"
  9. def aConfusedMap=[key1:"who am i?"]
  10. aConfuseMap中的key1到底是"key1"還是變量key1的值“wowo”?顯然,答案是字符串"key1"。如果要是"wowo"的話,則aConfusedMap的定義必須設置成:
  11. def aConfusedMap=[(key1):"who am i?"]
  12. Map中元素的存取更加方便,它支持多種方法:
  13. println aMap.keyName <==這種表達方法好像key就是aMap的一個成員變量一樣
  14. println aMap['keyName'] <==這種表達方法更傳統一點
  15. aMap.anotherkey = "i am map" <==爲map添加新元素

 

 

 

 

 

3.  Range類

Range是Groovy對List的一種拓展,變量定義和大體的使用方法如下:

 

  1. def aRange = 1..5 <==Range類型的變量 由begin值+兩個點+end值表示
  2. 左邊這個aRange包含1,2,3,4,55個值
  3. 如果不想包含最後一個元素,則
  4. def aRangeWithoutEnd = 1..<5 <==包含1,2,3,44個元素
  5. println aRange.from
  6. 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包中:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905193143290?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖5&nbsp; Range類API文檔</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

有了API文檔,你就可以放心調用其中的函數了。不過,不過,不過:我們剛纔代碼中用到了Range.from/to屬性值,但翻看Range API文檔的時候,其實並沒有這兩個成員變量。圖6是Range的方法

 

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905193206100?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖6&nbsp; Range類的方法</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

文檔中並沒有說明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  閉包

3.4.1  閉包的樣子

閉包,英文叫Closure,是Groovy中非常重要的一個數據類型或者說一種概念了。閉包的歷史來源,種種好處我就不說了。我們直接看怎麼使用它!

閉包,是一種數據類型,它代表了一段可執行的代碼。其外形如下:

 

  1. def aClosure = {//閉包是一段代碼,所以需要用花括號括起來..
  2. Stringparam1, int param2 -> //這個箭頭很關鍵。箭頭前面是參數定義,箭頭後面是代碼
  3. println"this is code" //這是代碼,最後一句是返回值,
  4. //也可以使用return,和Groovy中普通函數一樣
  5. }

 

 

 

 

 

簡而言之,Closure的定義格式是:

 

 
def xxx = {paramters -> code} //或者 def xxx = {無參數,純code} 這種case不需要->符號

 

 

 

 

 

 

 

 

 

說實話,從C/C++語言的角度看,閉包和函數指針很像。閉包定義好後,要調用它的方法就是:

閉包對象.call(參數)  或者更像函數指針調用的方法:

閉包對象(參數) 

比如:

 

  1. aClosure.call("this is string",100) 或者
  2. aClosure("this is string", 100)

 

 

 

 

 

上面就是一個閉包的定義和使用。在閉包中,還需要注意一點:

如果閉包沒定義參數的話,則隱含有一個參數,這個參數名字叫it,和this的作用類似。it代表閉包的參數。

 

  1. 比如:
  2. def greeting = { "Hello, $it!" }
  3. assert greeting('Patrick') == 'Hello, Patrick!'
  4. 等同於:
  5. def greeting = { it -> "Hello, $it!"}
  6. assert greeting('Patrick') == 'Hello, Patrick!'
  7. 但是,如果在閉包定義時,採用下面這種寫法,則表示閉包沒有參數!
  8. def noParamClosure = { -> true }
  9. 這個時候,我們就不能給noParamClosure傳參數了!
  10. noParamClosure ("test") <==報錯喔!

 

 

 

 

 

3.4.2  Closure使用中的注意點

1.  省略圓括號

閉包在Groovy中大量使用,比如很多類都定義了一些函數,這些函數最後一個參數都是一個閉包。比如:

 

  1. public static <T> List<T>each(List<T> self, Closure closure)
  2. 上面這個函數表示針對List的每一個元素都會調用closure做一些處理。這裏的closure,就有點回調函數的感覺。但是,在使用這個each函數的時候,我們傳遞一個怎樣的Closure進去呢?比如:
  3. def iamList = [1,2,3,4,5] //定義一個List
  4. iamList.each{ //調用它的each,這段代碼的格式看不懂了吧?each是個函數,圓括號去哪了?
  5. println it
  6. }
  7. 上面代碼有兩個知識點:
  8. l each函數調用的圓括號不見了!原來,Groovy中,當函數的最後一個參數是閉包的話,可以省略圓括號。比如
  9. def testClosure(int a1,String b1, Closure closure){
  10. //dosomething
  11. closure() //調用閉包
  12. }
  13. 那麼調用的時候,就可以免括號!
  14. testClosure (4, "test", {
  15. println"i am in closure"
  16. } ) //紅色的括號可以不寫..

 

 

 

 

 

注意,這個特點非常關鍵,因爲以後在Gradle中經常會出現圖7這樣的代碼:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905193225923?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖7&nbsp; 閉包調用</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

經常碰見圖7這樣的沒有圓括號的代碼。省略圓括號雖然使得代碼簡潔,看起來更像腳本語言,但是它這經常會讓我confuse(不知道其他人是否有同感),以doLast爲例,完整的代碼應該按下面這種寫法:

 

 

  1. doLast({
  2. println'Hello world!'
  3. })

 

 

 

 

 

有了圓括號,你會知道 doLast只是把一個Closure對象傳了進去。很明顯,它不代表這段腳本解析到doLast的時候就會調用println 'Hello world!'

但是把圓括號去掉後,就感覺好像println 'Hello world!'立即就會被調用一樣!

2.  如何確定Closure的參數

另外一個比較讓人頭疼的地方是,Closure的參數該怎麼搞?還是剛纔的each函數:

 

public static <T> List<T> each(List<T>self, Closure closure)

 

 

 

 

 

如何使用它呢?比如:

 

  1. def iamList = [1,2,3,4,5] //定義一個List變量
  2. iamList.each{ //調用它的each函數,只要傳入一個Closure就可以了。
  3. println it
  4. }

 

 

 

 

 

看起來很輕鬆,其實:

對於each所需要的Closure,它的參數是什麼?有多少個參數?返回值是什麼?

我們能寫成下面這樣嗎?

 

  1. iamList.each{String name,int x ->
  2. return x
  3. } //運行的時候肯定報錯!


所以,Closure雖然很方便,但是它一定會和使用它的上下文有極強的關聯。要不,作爲類似回調這樣的東西,我如何知道調用者傳遞什麼參數給Closure呢?

 

此問題如何破解?只能通過查詢API文檔才能瞭解上下文語義。比如下圖8:

 

		<p><img alt="" class="has" height="133" src="https://img-blog.csdn.net/20150905193242319?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" width="198"></p>

		<p>&nbsp;</p>

		<p><img alt="" class="has" height="126" src="https://img-blog.csdn.net/20150905193459310?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" width="200"></p>

		<p>圖8&nbsp; 文檔說明</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

 

圖8中:

 

 

  • each函數說明中,將給指定的closure傳遞Set中的每一個item。所以,closure的參數只有一個。
  • findAll中,絕對抓瞎了。一個是沒說明往Closure裏傳什麼。另外沒說明Closure的返回值是什麼.....。

 

對Map的findAll而言,Closure可以有兩個參數。findAll會將Key和Value分別傳進去。並且,Closure返回true,表示該元素是自己想要的。返回false表示該元素不是自己要找的。示意代碼如圖9所示:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905193642662?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖9&nbsp; Closure調用示例</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

Closure的使用有點坑,很大程度上依賴於你對API的熟悉程度,所以最初階段,SDK查詢是少不了的。

3.5  腳本類、文件I/O和XML操作

最後,我們來看一下Groovy中比較高級的用法。

3.5.1  腳本類

1.  腳本中import其他類

Groovy中可以像Java那樣寫package,然後寫類。比如在文件夾com/cmbc/groovy/目錄中放一個文件,叫Test.groovy,如圖10所示:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905193711206?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖10&nbsp; com/cmbc/groovy/Test.groovy文件</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

你看,圖10中的Test.groovy和Java類就很相似了。當然,如果不聲明public/private等訪問權限的話,Groovy中類及其變量默認都是public的。

 

現在,我們在測試的根目錄下建立一個test.groovy文件。其代碼如下所示:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905193733171?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖11&nbsp; test.groovy訪問com/cmbc/groovy包</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

你看,test.groovy先import了com.cmbc.groovy.Test類,然後創建了一個Test類型的對象,接着調用它的print函數。

 

這兩個groovy文件的目錄結構如圖12所示:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905193800825?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖12&nbsp; Test.groovy和test.groovy目錄結構</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

在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的編譯命令,-dclasses用於將編譯得到的class文件拷貝到classes文件夾下

圖13是test.groovy腳本轉換得到的java class。用jd-gui反編譯它的代碼:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905193818456?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖13 groovy腳本反編譯得到的Java類源碼</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

圖13中:

 

 

  • test.groovy被轉換成了一個test類,它從script派生。
  • 每一個腳本都會生成一個static main函數。這樣,當我們groovytest.groovy的時候,其實就是用java去執行這個main函數
  • 腳本中的所有代碼都會放到run函數中。比如,println 'Groovy world',這句代碼實際上是包含在run函數裏的。
  • 如果腳本中定義了函數,則函數會被定義在test類中。

 

groovyc是一個比較好的命令,讀者要掌握它的用法。然後利用jd-gui來查看對應class的Java源碼。

3.  腳本中的變量和作用域

前面說了,xxx.groovy只要不是和Java那樣的class,那麼它就是一個腳本。而且腳本的代碼其實都會被放到run函數中去執行。那麼,在Groovy的腳本中,很重要的一點就是腳本中定義的變量和它的作用域。舉例:

 

  1. def x = 1 <==注意,這個x有def(或者指明類型,比如 int x = 1
  2. def printx(){
  3. println x
  4. }

 

 

 

 

 

printx() <==報錯,說x找不到

爲什麼?繼續來看反編譯後的class文件。

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905193919401?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖14&nbsp; 反編譯後的test.class文件</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

圖14中:

 

 

  1. l printx被定義成test類的成員函數
  2. l def x = 1,這句話是在run中創建的。所以,x=1從代碼上看好像是在整個腳本中定義的,但實際上printx訪問不了它。printx是test成員函數,除非x也被定義成test的成員函數,否則printx不能訪問它。
  3. 那麼,如何使得printx能訪問x呢?很簡單,定義的時候不要加類型和def。即:
  4. x = 1 <==注意,去掉def或者類型
  5. def printx(){
  6. println x
  7. }

 

 

 

 

 

printx() <==OK

這次Java源碼又變成什麼樣了呢?

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905193938259?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖15&nbsp; 進化版的test.groovy</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

圖15中,x也沒有被定義成test的成員函數,而是在run的執行過程中,將x作爲一個屬性添加到test實例對象中了。然後在printx中,先獲取這個屬性。

 

注意,Groovy的文檔說 x = 1這種定義將使得x變成test的成員變量,但從反編譯情況看,這是不對得.....

雖然printx可以訪問x變量了,但是假如有其他腳本卻無法訪問x變量。因爲它不是test的成員變量。

比如,我在測試目錄下創建一個新的名爲test1.groovy。這個test1將訪問test.groovy中定義的printx函數:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905193959378?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖16&nbsp; test1.groovy使用test.groovy中的函數</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

這種方法使得我們可以將代碼分成模塊來編寫,比如將公共的功能放到test.groovy中,然後使用公共功能的代碼放到test1.groovy中

 

執行groovy test1.groovy,報錯。說x找不到。這是因爲x是在test的run函數動態加進去的。怎麼辦?

 

  1. import groovy.transform.Field; //必須要先import
  2. @Field x = 1 <==在x前面加上@Field標註,這樣,x就徹徹底底是test的成員變量了。

 

 

 

 

 

查看編譯後的test.class文件,得到:

		<p>&nbsp;</p>

		<p>圖17&nbsp; x現在是test類的成員變量了</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

這個時候,test.groovy中的x就成了test類的成員函數了。如此,我們可以在script中定義那些需要輸出給外部腳本或類使用的變量了!

 

 

3.5.2  文件I/O操作

本節介紹下Groovy的文件I/O操作。直接來看例子吧,雖然比Java看起來簡單,但要理解起來其實比較難。尤其是當你要自己查SDK並編寫代碼的時候。

整體說來,Groovy的I/O操作是在原有Java I/O操作上進行了更爲簡單方便的封裝,並且使用Closure來簡化代碼編寫。主要封裝瞭如下一些了類:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905194032351?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖18&nbsp; Groovy File I/o常用類和SDK文檔位置</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

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

 

  1. targetFile.eachLine{
  2. StringoneLine ->
  3. printlnoneLine
  4. } <==是不是令人髮指??!

 

 

 

 

 

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

 

  1. def ism = targetFile.newInputStream()
  2. //操作ism,最後記得關掉
  3. ism.close

 

 

 

 

 

 

4 使用閉包操作inputStream,以後在Gradle裏會常看到這種搞法

 

  1. targetFile.withInputStream{ ism ->
  2. 操作ism. 不用close。Groovy會自動替你close
  3. }

 

 

 

 

 

確實夠簡單,令人髮指。我當年死活也沒找到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文件。

 

  1. def srcFile = new File(源文件名)
  2. def targetFile = new File(目標文件名)
  3. targetFile.withOutputStream{ os->
  4. srcFile.withInputStream{ ins->
  5. os << ins //利用OutputStream的<<操作符重載,完成從inputstream到OutputStream
  6. //的輸出
  7. }
  8. }

 

 

 

 

 

尼瑪....關於OutputStream的<<操作符重載,查看SDK文檔後可知:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905194055193?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖19&nbsp; OutputStream的&lt;&lt;操作符重載</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

再一次向極致簡單致敬。但是,SDK恐怕是離不開手了...

 

 

3.5.3  XML操作

除了I/O異常簡單之外,Groovy中的XML操作也極致得很。Groovy中,XML的解析提供了和XPath類似的方法,名爲GPath。這是一個類,提供相應API。關於XPath,請腦補https://en.wikipedia.org/wiki/XPath。

GPath功能包括:給個例子好了,來自Groovy官方文檔。

 

  1. test.xml文件:
  2. <response version-api="2.0">
  3. <value>
  4. <books>
  5. <book available="20" id="1">
  6. <title>Don Xijote</title>
  7. <author id="1">Manuel De Cervantes</author>
  8. </book>
  9. <book available="14" id="2">
  10. <title>Catcher in the Rye</title>
  11. <author id="2">JD Salinger</author>
  12. </book>
  13. <book available="13" id="3">
  14. <title>Alice in Wonderland</title>
  15. <author id="3">Lewis Carroll</author>
  16. </book>
  17. <book available="5" id="4">
  18. <title>Don Xijote</title>
  19. <author id="4">Manuel De Cervantes</author>
  20. </book>
  21. </books>
  22. </value>
  23. </response>

 

 

 

 

 

現在來看怎麼玩轉GPath:

 

  1. //第一步,創建XmlSlurper類
  2. def xparser = new XmlSlurper()
  3. def targetFile = new File("test.xml")
  4. //轟轟的GPath出場
  5. GPathResult gpathResult =xparser.parse(targetFile)
  6. //開始玩test.xml。現在我要訪問id=4的book元素。
  7. //下面這種搞法,gpathResult代表根元素response。通過e1.e2.e3這種
  8. //格式就能訪問到各級子元素....
  9. def book4 = gpathResult.value.books.book[3]
  10. //得到book4的author元素
  11. def author = book4.author
  12. //再來獲取元素的屬性和textvalue
  13. assert author.text() == ' Manuel De Cervantes '
  14. 獲取屬性更直觀
  15. author.@id == '4' 或者 author['@id'] == '4'
  16. 屬性一般是字符串,可通過toInteger轉換成整數
  17. author.@id.toInteger() == 4
  18. 好了。GPath就說到這。再看個例子。我在使用Gradle的時候有個需求,就是獲取AndroidManifest.xml版本號(versionName)。有了GPath,一行代碼搞定,請看:
  19. def androidManifest = newXmlSlurper().parse("AndroidManifest.xml")
  20. println androidManifest['@android:versionName']
  21. 或者
  22. println androidManifest.@'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。其中的UserGuideDSL Reference很關鍵。User Guide就是介紹Gradle的一本書,而DSLReference是Gradle API的說明。

以Ubuntu爲例,下載Gradle:http://gradle.org/gradle-download/  選擇Completedistribution和Binary only distribution都行。然後解壓到指定目錄。

最後,設置~/.bashrc,把Gradle加到PATH裏,如圖20所示:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905194121511?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖20&nbsp; 配置Gradle到bashrc</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

執行source ~/.bashrc,初始化環境。

 

執行gradle --version,如果成功運行就OK了。

注意,爲什麼說Gradle是一個編程框架?來看它提供的API文檔:

https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905194136650?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖21&nbsp; Project接口說明</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

原來,我們編寫所謂的編譯腳本,其實就是玩Gradle的API....所以它從更底層意義上看,是一個編程框架!

 

既然是編程框架,我在講解Gradle的時候,儘量會從API的角度來介紹。有些讀者肯定會不耐煩,爲嘛這麼費事?

從我個人的經歷來看:因爲我從網上學習到的資料來看,幾乎全是從腳本的角度來介紹Gradle,結果學習一通下來,只記住參數怎麼配置,卻不知道它們都是函數調用,都是嚴格對應相關API的。

而從API角度來看待Gradle的話,有了SDK文檔,你就可以編程。編程是靠記住一行行代碼來實現的嗎?不是,是在你掌握大體流程,然後根據SDK+API來完成的!

其實,Gradle自己的User Guide也明確說了

Buildscripts 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工程。

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905194157628?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖22 重要例子</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

在圖22的例子中:

 

 

  • CPosDeviceSdk、CPosSystemSdk、CPosSystemSdkxxxImpl是Android Library。其中,CPosSystemSdkxxxImpl依賴CPosSystemSdk
  • CPosDeviceServerApk和CPosSdkDemo是Android APP。這些App和SDK有依賴關係。CPosDeviceServerApk依賴CPosDeviceSdk,而CPosSdkDemo依賴所有的Sdk Library。

 

請回答問題,在上面這個例子中,有多少個Project

請回答問題,在上面這個例子中,有多少個Project

請回答問題,在上面這個例子中,有多少個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會生成最終的產物,所以gradleassemble)

這很麻煩啊,有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]

 

  1. //通過include函數,將子Project的名字(其文件夾名)包含進來
  2. include 'CPosSystemSdk' ,'CPosDeviceSdk' ,
  3. 'CPosSdkDemo','CPosDeviceServerApk','CPosSystemSdkWizarPosImpl'

 

 

 

 

 

 

強烈建議:

如果你確實只有一個Project需要編譯,我也建議你在目錄下添加一個settings.gradle。我們團隊內部的所有單個Project都已經改成支持Multiple-Project Build了。改得方法就是添加settings.gradle,然後include對應的project名字。

另外,settings.gradle除了可以include外,還可以設置一些函數。這些函數會在gradle構建整個工程任務的時候執行,所以,可以在settings做一些初始化的工作。比如:我的settings.gradle的內容:

//定義一個名爲initMinshengGradleEnvironment的函數。該函數內部完成一些初始化操作

//比如創建特定的目錄,設置特定的參數等

 

  1. def initMinshengGradleEnvironment(){
  2. println"initialize Minsheng Gradle Environment ....."
  3. ......//幹一些special的私活....
  4. println"initialize Minsheng Gradle Environment completes..."
  5. }
  6. //settings.gradle加載的時候,會執行initMinshengGradleEnvironment
  7. initMinshengGradleEnvironment()
  8. //include也是一個函數:
  9. include 'CPosSystemSdk' , 'CPosDeviceSdk' ,
  10. 'CPosSdkDemo','CPosDeviceServerApk','CPosSystemSdkWizarPosImpl'

 

 

 

 

 

4.2.2  gradle命令介紹

1.  gradle projects查看工程信息

到目前爲止,我們瞭解了Gradle什麼呢?

每一個Project都必須設置一個build.gradle文件。至於其內容,我們留到後面再說。

對於multi-projects build,需要在根目錄下也放一個build.gradle,和一個settings.gradle。

一個Project是由若干tasks來組成的,當gradlexxx的時候,實際上是要求gradle執行xxx任務。這個任務就能完成具體的工作。

當然,具體的工作和不同的插件有關係。編譯Java要使用Java插件,編譯Android APP需要使用Android APP插件。這些我們都留待後續討論

gradle提供一些方便命令來查看和Project,Task相關的信息。比如在posdevice中,我想看這個multi projects到底包含多少個子Project:

執行gradle projects,得到圖23:

		<p>&nbsp;</p>

		<p>圖23&nbsp; gradle projects</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

你看,multi projects的情況下,posdevice這個目錄對應的build.gradle叫Root

 

 Project,它包含5個子Project。

如果你修改settings.gradle,使得include只有一個參數,則gradle projects的子project也會變少,比如圖24:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905194231875?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖24&nbsp; 修改settings.gradle,使得只包含CPosSystemSdk工程</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

2.  gradle tasks查看任務信息

查看了Project信息,這個還比較簡單,直接看settings.gradle也知道。那麼Project包含哪些Task信息,怎麼看呢?圖23,24中最後的輸出也告訴你了,想看某個Project包含哪些Task信息,只要執行:

gradleproject-path:tasks 就行。注意,project-path是目錄名,後面必須跟冒號。

對於Multi-project,在根目錄中,需要指定你想看哪個poject的任務。不過你要是已經cd到某個Project的目錄了,則不需指定Project-path。

來看圖25:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905194251964?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖25&nbsp; gradle CPosSystemSdk:tasks</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

圖25是gradleCPosSystemSdk:tasks的結果。

 

 

  • cd CPossystemSdk
  • gradle tasks 得到同樣的結果

 

CPosSystemSdk是一個Android Library工程,Android Library對應的插件定義了好多Task。每種插件定義的Task都不盡相同,這就是所謂的Domain Specific,需要我們對相關領域有比較多的瞭解。

這些都是後話,我們以後會詳細介紹。

3.  gradle task-name執行任務

圖25中列出了好多任務,這時候就可以通過 gradle 任務名來執行某個任務。這和make xxx很像。比如:

 

  • gradle clean是執行清理任務,和make clean類似。
  • 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!

 

4.3  Gradle工作流程

Gradle的工作流程其實蠻簡單,用一個圖26來表達:

		<p>圖26&nbsp; Gradle工作流程</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

圖26告訴我們,Gradle工作包含三個階段:

 

 

  • 首先是初始化階段。對我們前面的multi-project build而言,就是執行settings.gradle
  • Initiliazation phase的下一個階段是Configration階段。
  • Configration階段的目標是解析每個project中的build.gradle。比如multi-project build例子中,解析每個子目錄中的build.gradle。在這兩個階段之間,我們可以加一些定製化的Hook。這當然是通過API來添加的。
  • Configuration階段完了後,整個build的project以及內部的Task關係就確定了。恩?前面說過,一個Project包含很多Task,每個Task之間有依賴關係。Configuration會建立一個有向圖來描述Task之間的依賴關係。所以,我們可以添加一個HOOK,即當Task關係圖建立好後,執行一些操作。
  • 最後一個階段就是執行任務了。當然,任務執行完後,我們還可以加Hook。

 

下面展示一下我按圖26爲posdevice項目添加的Hook,它的執行結果:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905194338602?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖26&nbsp; 加了Hook後的執行結果</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

我在:

 

 

  • settings.gradle加了一個輸出。
  • 在posdevice的build.gradle加了圖25中的beforeProject函數。
  • 在CPosSystemSdk加了taskGraph whenReady函數和buidFinished函數。

 

好了,Hook的代碼怎麼寫,估計你很好奇,而且肯定會埋汰,搞毛這麼就還沒告訴我怎麼寫Gradle。馬上了!

最後,關於Gradle的工作流程,你只要記住:

 

  • Gradle有一個初始化流程,這個時候settings.gradle會執行。
  • 在配置階段,每個Project都會被解析,其內部的任務也會被添加到一個有向圖裏,用於解決執行過程中的依賴關係。
  • 然後纔是執行階段。你在gradle xxx中指定什麼任務,gradle就會將這個xxx任務鏈上的所有任務全部按依賴順序執行一遍!

 

下面來告訴你怎麼寫代碼!

4.4  Gradle編程模型及API實例詳解

希望你在進入此節之前,一定花時間把前面內容看一遍!!!

https://docs.gradle.org/current/dsl/  <==這個文檔很重要

Gradle基於Groovy,Groovy又基於Java。所以,Gradle執行的時候和Groovy一樣,會把腳本轉換成Java對象。Gradle主要有三種對象,這三種對象和三種不同的腳本文件對應,在gradle執行的時候,會將腳本轉換成對應的對端:

 

  • Gradle對象:當我們執行gradle xxx或者什麼的時候,gradle會從默認的配置腳本中構造出一個Gradle對象。在整個執行過程中,只有這麼一個對象。Gradle對象的數據類型就是Gradle。我們一般很少去定製這個默認的配置腳本。
  • Project對象:每一個build.gradle會轉換成一個Project對象。
  • Settings對象:顯然,每一個settings.gradle都會轉換成一個Settings對象。

 

注意,對於其他gradle文件,除非定義了class,否則會轉換成一個實現了Script接口的對象。這一點和3.5節中Groovy的腳本類相似

當我們執行gradle的時候,gradle首先是按順序解析各個gradle文件。這裏邊就有所所謂的生命週期的問題,即先解析誰,後解析誰。圖27是Gradle文檔中對生命週期的介紹:結合上一節的內容,相信大家都能看明白了。現在只需要看紅框裏的內容:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905194405209?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖27&nbsp; Gradle對LifeCycle的介紹</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

4.4.1  Gradle對象

我們先來看Gradle對象,它有哪些屬性呢?如圖28所示:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905194421970?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖28&nbsp; Gradle的屬性</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

我在posdevice build.gradle中和settings.gradle中分別加了如下輸出:

 

 

  1. //在settings.gradle中,則輸出"In settings,gradle id is"
  2. println "In posdevice, gradle id is " +gradle.hashCode()
  3. println "Home Dir:" + gradle.gradleHomeDir
  4. println "User Home Dir:" + gradle.gradleUserHomeDir
  5. println "Parent: " + gradle.parent

 

 

 

 

 

得到結果如圖29所示:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905194440047?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖29&nbsp; gradle示例</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

 

 

  • 你看,在settings.gradle和posdevice build.gradle中,我們得到的gradle實例對象的hashCode是一樣的(都是791279786)。
  • HomeDir是我在哪個目錄存儲的gradle可執行程序。
  • User Home Dir:是gradle自己設置的目錄,裏邊存儲了一些配置文件,以及編譯過程中的緩存文件,生成的類文件,編譯中依賴的插件等等。

 

Gradle的函數接口在文檔中也有。

4.4.2  Project對象

每一個build.gradle文件都會轉換成一個Project對象。在Gradle術語中,Project對象對應的是BuildScript

Project包含若干Tasks。另外,由於Project對應具體的工程,所以需要爲Project加載所需要的插件,比如爲Java工程加載Java插件。其實,一個Project包含多少Task往往是插件決定的。

所以,在Project中,我們要:

 

  • 加載插件。
  • 不同插件有不同的行話,即不同的配置。我們要在Project中配置好,這樣插件就知道從哪裏讀取源文件等
  • 設置屬性。

 

1.  加載插件

Project的API位於https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html。加載插件是調用它的apply函數.apply其實是Project實現的PluginAware接口定義的:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905194503216?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖30&nbsp; apply函數</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

來看代碼:

 

[apply函數的用法]

apply是一個函數,此處調用的是圖30中最後一個apply函數。注意,Groovy支持

函數調用的時候通過  參數名1:參數值2,參數名2:參數值2 的方式來傳遞參數

 

  1. apply plugin: 'com.android.library' <==如果是編譯Library,則加載此插件
  2. 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說明:

		<p>&nbsp;</p>

		<p>圖31&nbsp; apply API說明</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

我這裏不遺餘力的列出API圖片,就是希望大家在寫腳本的時候,碰到不會的,一定要去查看API文檔!

 

2.  設置屬性

如果是單個腳本,則不需要考慮屬性的跨腳本傳播,但是Gradle往往包含不止一個build.gradle文件,比如我設置的utils.gradle,settings.gradle。如何在多個腳本中設置屬性呢?

Gradle提供了一種名爲extra property的方法。extra property是額外屬性的意思,在第一次定義該屬性的時候需要通過ext前綴來標示它是一個額外的屬性。定義好之後,後面的存取就不需要ext前綴了。ext屬性支持Project和Gradle對象。即Project和Gradle對象都可以設置ext屬性

舉個例子:

我在settings.gradle中想爲Gradle對象設置一些外置屬性,所以在initMinshengGradleEnvironment函數中

 

  1. def initMinshengGradleEnvironment(){
  2. //屬性值從local.properites中讀取
  3. Propertiesproperties = new Properties()
  4. FilepropertyFile = new File(rootDir.getAbsolutePath() +"/local.properties")
  5. properties.load(propertyFile.newDataInputStream())
  6. //gradle就是gradle對象。它默認是Settings和Project的成員變量。可直接獲取
  7. //ext前綴,表明操作的是外置屬性。api是一個新的屬性名。前面說過,只在
  8. //第一次定義或者設置它的時候需要ext前綴
  9. gradle.ext.api =properties.getProperty('sdk.api')
  10. println gradle.api //再次存取api的時候,就不需要ext前綴了
  11. ......
  12. }

 

 

 

 

 

再來一個例子強化一下:

我在utils.gradle中定義了一些函數,然後想在其他build.gradle中調用這些函數。那該怎麼做呢?

[utils.gradle]

 

  1. //utils.gradle中定義了一個獲取AndroidManifests.xmlversionName的函數
  2. def getVersionNameAdvanced(){
  3. 下面這行代碼中的project是誰?
  4. defxmlFile = project.file("AndroidManifest.xml")
  5. defrootManifest = new XmlSlurper().parse(xmlFile)
  6. returnrootManifest['@android:versionName']
  7. }
  8. //現在,想把這個API輸出到各個Project。由於這個utils.gradle會被每一個Project Apply,所以
  9. //我可以把getVersionNameAdvanced定義成一個closure,然後賦值到一個外部屬性
  10. 下面的ext是誰的ext?
  11. ext{ //此段花括號中代碼是閉包
  12. //除了ext.xxx=value這種定義方法外,還可以使用ext{}這種書寫方法。
  13. //ext{}不是ext(Closure)對應的函數調用。但是ext{}中的{}確實是閉包。
  14. getVersionNameAdvanced = this.&getVersionNameAdvanced
  15. }

 

 

 

 

 

上面代碼中有兩個問題:

 

  •   project是誰?
  •   ext是誰的ext?

 

上面兩個問題比較關鍵,我也是花了很長時間才搞清楚。這兩個問題歸結到一起,其實就是:

加載utils.gradle的Project對象和utils.gradle本身所代表的Script對象到底有什麼關係?

我們在Groovy中也講過怎麼在一個Script中import另外一個Script中定義的類或者函數(見3.5 腳本類、文件I/O和XML操作一節)。在Gradle中,這一塊的處理比Groovy要複雜,具體怎麼搞我還沒完全弄清楚,但是Project和utils.gradle對於的Script的對象的關係是:

 

  • 當一個Project apply一個gradle文件的時候,這個gradle文件會轉換成一個Script對象。這個,相信大家都已經知道了。
  • Script中有一個delegate對象,這個delegate默認是加載(即調用apply)它的Project對象。但是,在apply函數中,有一個from參數,還有一個to參數(參考圖31)。通過to參數,你可以把delegate對象指定爲別的東西。
  • delegate對象是什麼意思?當你在Script中操作一些不是Script自己定義的變量,或者函數時候,gradle會到Script的delegate對象去找,看看有沒有定義這些變量或函數。

 

現在你知道問題1,2和答案了:

 

  • 問題1:project就是加載utils.gradle的project。由於posdevice有5個project,所以utils.gradle會分別加載到5個project中。所以,getVersionNameAdvanced纔不用區分到底是哪個project。反正一個project有一個utils.gradle對應的Script。
  • 問題2:ext:自然就是Project對應的ext了。此處爲Project添加了一些closure。那麼,在Project中就可以調用getVersionNameAdvanced函數了

 

比如:我在posdevice每個build.gradle中都有如下的代碼:

 

  1. tasks.getByName("assemble"){
  2. it.doLast{
  3. println "$project.name: After assemble, jar libs are copied tolocal repository"
  4. copyOutput(true) //copyOutput是utils.gradle輸出的closure
  5. }
  6. }

 

 

 

 

 

通過這種方式,我將一些常用的函數放到utils.gradle中,然後爲加載它的Project設置ext屬性。最後,Project中就可以調用這種賦值函數了!

注意:此處我研究的還不是很深,而且我個人感覺:

1  在Java和Groovy中:我們會把常用的函數放到一個輔助類和公共類中,然後在別的地方import並調用它們。

2  但是在Gradle,更正規的方法是在xxx.gradle中定義插件。然後通過添加Task的方式來完成工作。gradle的user guide有詳細介紹如何實現自己的插件。

3.  Task介紹

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]

/

  1. /Task是和Project關聯的,所以,我們要利用Project的task函數來創建一個Task
  2. task myTask <==myTask是新建Task的名字
  3. task myTask { configure closure }
  4. task myType << { task action } <==注意,<<符號是doLast的縮寫
  5. task myTask(type: SomeType)
  6. task myTask(type: SomeType) { configure closure }

 

 

 

上述代碼中都用了Project的一個函數,名爲task,注意:

 

  • 一個Task包含若干Action。所以,Task有doFirst和doLast兩個函數,用於添加需要最先執行的Action和需要和需要最後執行的Action。Action就是一個閉包。
  • Task創建的時候可以指定Type,通過type:名字表達。這是什麼意思呢?其實就是告訴Gradle,這個新建的Task對象會從哪個基類Task派生。比如,Gradle本身提供了一些通用的Task,最常見的有Copy 任務。Copy是Gradle中的一個類。當我們:task myTask(type:Copy)的時候,創建的Task就是一個Copy Task。
  • 當我們使用 taskmyTask{ xxx}的時候。花括號是一個closure。這會導致gradle在創建這個Task之後,返回給用戶之前,會先執行closure的內容。
  • 當我們使用taskmyTask << {xxx}的時候,我們創建了一個Task對象,同時把closure做爲一個action加到這個Task的action隊列中,並且告訴它“最後才執行這個closure”(注意,<<符號是doLast的代表)。

 

圖32是Project中關於task函數說明:

		<p>&nbsp;</p>

		<p>圖32&nbsp; Project中task函數</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

陸陸續續講了這麼些內容,我自己感覺都有點煩了。是得,Gradle用一整本書來講都嫌不夠呢。

 

anyway,到目前爲止,我介紹的都是一些比較基礎的東西,還不是特別多。但是後續例子該涉及到的知識點都有了。下面我們直接上例子。這裏有兩個例子:

 

  • posdevice的例子
  • 另外一個是單個project的例子

 

4.4.3  posdevice實例

現在正是開始通過例子來介紹怎麼玩gradle。這裏要特別強調一點,根據Gradle的哲學。gradle文件中包含一些所謂的Script Block(姑且這麼稱它)。ScriptBlock作用是讓我們來配置相關的信息。不同的SB有不同的需要配置的東西。這也是我最早說的行話。比如,源碼對應的SB,就需要我們配置源碼在哪個文件夾裏。關於SB,我們後面將見識到!

posdevice是一個multi project。下面包含5個Project。對於這種Project,請大家回想下我們該創建哪些文件?

 

  • settings.gradle是必不可少的
  • 根目錄下的build.gradle。這個我們沒講過,因爲posdevice的根目錄本身不包含代碼,而是包含其他5個子project。
  • 每個project目錄下包含對於的build.gradle
  • 另外,我把常用的函數封裝到一個名爲utils.gradle的腳本里了。

 

馬上一個一個來看它們。

1.  utils.gradle

utils.gradle是我自己加的,爲我們團隊特意加了一些常見函數。主要代碼如下:

[utils.gradle]

 

  1. import groovy.util.XmlSlurper //解析XML時候要引入這個groovy的package
  2. def copyFile(String srcFile,dstFile){
  3. ......//拷貝文件函數,用於將最後的生成物拷貝到指定的目錄
  4. }
  5. def rmFile(String targetFile){
  6. .....//刪除指定目錄中的文件
  7. }
  8. def cleanOutput(boolean bJar = true){
  9. ....//clean的時候清理
  10. }
  11. def copyOutput(boolean bJar = true){
  12. ....//copyOutput內部會調用copyFile完成一次build的產出物拷貝
  13. }
  14. def getVersionNameAdvanced(){//老朋友
  15. defxmlFile = project.file("AndroidManifest.xml")
  16. defrootManifest = new XmlSlurper().parse(xmlFile)
  17. returnrootManifest['@android:versionName']
  18. }
  19. //對於android library編譯,我會disable所有的debug編譯任務
  20. def disableDebugBuild(){
  21. //project.tasks包含了所有的tasks,下面的findAll是尋找那些名字中帶debug的Task。
  22. //返回值保存到targetTasks容器中
  23. def targetTasks = project.tasks.findAll{task ->
  24. task.name.contains("Debug")
  25. }
  26. //對滿足條件的task,設置它爲disable。如此這般,這個Task就不會被執行
  27. targetTasks.each{
  28. println"disable debug task :${it.name}"
  29. it.setEnabled false
  30. }
  31. }
  32. //將函數設置爲extra屬性中去,這樣,加載utils.gradle的Project就能調用此文件中定義的函數了
  33. ext{
  34. copyFile= this.&copyFile
  35. rmFile =this.&rmFile
  36. cleanOutput = this.&cleanOutput
  37. copyOutput = this.&copyOutput
  38. getVersionNameAdvanced = this.&getVersionNameAdvanced
  39. disableDebugBuild = this.&disableDebugBuild
  40. }

 

 

 

 

 

圖33展示了被disable的Debug任務的部分信息:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905194607997?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖33&nbsp; disable的Debug Task信息</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

2.  settings.gradle

這個文件中我們該幹什麼?調用include把需要包含的子Project加進來。代碼如下:

[settings.gradle]

 

  1. /*我們團隊內部建立的編譯環境初始化函數
  2. 這個函數的目的是
  3. 1 解析一個名爲local.properties的文件,讀取AndroidSDK和NDK的路徑
  4. 2 獲取最終產出物目錄的路徑。這樣,編譯完的apk或者jar包將拷貝到這個最終產出物目錄中
  5. 3 獲取Android SDK指定編譯的版本
  6. */
  7. def initMinshengGradleEnvironment(){
  8. println"initialize Minsheng Gradle Environment ....."
  9. Properties properties = new Properties()
  10. //local.properites也放在posdevice目錄下
  11. FilepropertyFile = new File(rootDir.getAbsolutePath()+ "/local.properties")
  12. properties.load(propertyFile.newDataInputStream())
  13. /*
  14. 根據Project、Gradle生命週期的介紹,settings對象的創建位於具體Project創建之前
  15. 而Gradle底對象已經創建好了。所以,我們把local.properties的信息讀出來後,通過
  16. extra屬性的方式設置到gradle對象中
  17. 而具體Project在執行的時候,就可以直接從gradle對象中得到這些屬性了!
  18. */
  19. gradle.ext.api =properties.getProperty('sdk.api')
  20. gradle.ext.sdkDir =properties.getProperty('sdk.dir')
  21. gradle.ext.ndkDir =properties.getProperty('ndk.dir')
  22. gradle.ext.localDir =properties.getProperty('local.dir')
  23. //指定debugkeystore文件的位置,debug版apk簽名的時候會用到
  24. gradle.ext.debugKeystore= properties.getProperty('debug.keystore')
  25. ......
  26. println"initialize Minsheng Gradle Environment completes..."
  27. }
  28. //初始化
  29. initMinshengGradleEnvironment()
  30. //添加子Project信息
  31. 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.dirndk.dir是Android Gradle必須要指定的,其他都是我自己加的屬性。當然。不編譯ndk,就不需要ndk.dir屬性了。

3.  posdevicebuild.gradle

作爲multi-project根目錄,一般情況下,它的build.gradle是做一些全局配置。來看我的build.gradle

[posdevicebuild.gradle]

 

  1. //下面這個subprojects{}就是一個Script Block
  2. subprojects {
  3. println"Configure for $project.name" //遍歷子Project,project變量對應每個子Project
  4. buildscript { //這也是一個SB
  5. repositories {//repositories是一個SB
  6. ///jcenter是一個函數,表示編譯過程中依賴的庫,所需的插件可以在jcenter倉庫中
  7. //下載。
  8. jcenter()
  9. }
  10. dependencies { //SB
  11. //dependencies表示我們編譯的時候,依賴android開發的gradle插件。插件對應的
  12. //class path是com.android.tools.build。版本是1.2.3
  13. classpath'com.android.tools.build:gradle:1.2.3'
  14. }
  15. //爲每個子Project加載utils.gradle 。當然,這句話可以放到buildscript花括號之後
  16. applyfrom: rootProject.getRootDir().getAbsolutePath() + "/utils.gradle"
  17. }//buildscript結束
  18. }

 

 

 

 

 

感覺解釋得好蒼白,SB在Gradle的API文檔中也是有的。先來看Gradle定義了哪些SB。如圖34所示:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905194626346?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖34 SB的類型</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

你看,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所示:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905194642357?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖35&nbsp;&nbsp; SB對應的函數</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

特別提示:當你下次看到一個不認識的SB的時候,就去看API吧。

 

下面來解釋代碼中的各個SB:

 

  • subprojects:它會遍歷posdevice中的每個子Project。在它的Closure中,默認參數是子Project對應的Project對象。由於其他SB都在subprojects花括號中,所以相當於對每個Project都配置了一些信息。
  • buildscript:它的closure是在一個類型爲ScriptHandler的對象上執行的。主意用來所依賴的classpath等信息。通過查看ScriptHandler API可知,在buildscript SB中,你可以調用ScriptHandler提供的repositories(Closure )、dependencies(Closure)函數。這也是爲什麼repositories和dependencies兩個SB爲什麼要放在buildscript的花括號中的原因。明白了?這就是所謂的行話,得知道規矩。不知道規矩你就亂了。記不住規矩,又不知道查SDK,那麼就徹底抓瞎,只能到網上到處找答案了!
  • 關於repositories和dependencies,大家直接看API吧。後面碰到了具體代碼我們再來介紹

 

4.  CPosDeviceSdkbuild.gradle

CPosDeviceSdk是一個Android Library。按Google的想法,Android Library編譯出來的應該是一個AAR文件。但是我的項目有些特殊,我需要發佈CPosDeviceSdk.jar包給其他人使用。jar在編譯過程中會生成,但是它不屬於Android Library的標準輸出。在這種情況下,我需要在編譯完成後,主動copy jar包到我自己設計的產出物目錄中。

 

  1. //Library工程必須加載此插件。注意,加載了Android插件就不要加載Java插件了。因爲Android
  2. //插件本身就是拓展了Java插件
  3. apply plugin: 'com.android.library'
  4. //android的編譯,增加了一種新類型的ScriptBlock-->android
  5. android {
  6. //你看,我在local.properties中設置的API版本號,就可以一次設置,多個Project使用了
  7. //藉助我特意設計的gradle.ext.api屬性
  8. compileSdkVersion =gradle.api //這兩個紅色的參數必須設置
  9. buildToolsVersion = "22.0.1"
  10. sourceSets{ //配置源碼路徑。這個sourceSets是Java插件引入的
  11. main{ //main:Android也用了
  12. manifest.srcFile 'AndroidManifest.xml' //這是一個函數,設置manifest.srcFile
  13. aidl.srcDirs=['src'] //設置aidl文件的目錄
  14. java.srcDirs=['src'] //設置java文件的目錄
  15. }
  16. }
  17. dependencies { //配置依賴關係
  18. //compile表示編譯和運行時候需要的jar包,fileTree是一個函數,
  19. //dir:'libs',表示搜索目錄的名稱是libs。include:['*.jar'],表示搜索目錄下滿足*.jar名字的jar
  20. //包都作爲依賴jar文件
  21. compile fileTree(dir: 'libs', include: ['*.jar'])
  22. }
  23. } //android SB配置完了
  24. //clean是一個Task的名字,這個Task好像是Java插件(這裏是Android插件)引入的。
  25. //dependsOn是一個函數,下面這句話的意思是 clean任務依賴cposCleanTask任務。所以
  26. //當你gradle clean以執行clean Task的時候,cposCleanTask也會執行
  27. clean.dependsOn 'cposCleanTask'
  28. //創建一個Task,
  29. task cposCleanTask() <<{
  30. cleanOutput(true) //cleanOutput是utils.gradle中通過extra屬性設置的Closure
  31. }
  32. //前面說了,我要把jar包拷貝到指定的目錄。對於Android編譯,我一般指定gradle assemble
  33. //它默認編譯debug和release兩種輸出。所以,下面這個段代碼表示:
  34. //tasks代表一個Projects中的所有Task,是一個容器。getByName表示找到指定名稱的任務。
  35. //我這裏要找的assemble任務,然後我通過doLast添加了一個Action。這個Action就是copy
  36. //產出物到我設置的目標目錄中去
  37. tasks.getByName("assemble"){
  38. it.doLast{
  39. println "$project.name: After assemble, jar libs are copied tolocal repository"
  40. copyOutput(true)
  41. }
  42. }
  43. /*
  44. 因爲我的項目只提供最終的release編譯出來的Jar包給其他人,所以不需要編譯debug版的東西
  45. 當Project創建完所有任務的有向圖後,我通過afterEvaluate函數設置一個回調Closure。在這個回調
  46. Closure裏,我disable了所有Debug的Task
  47. */
  48. project.afterEvaluate{
  49. disableDebugBuild()
  50. }

 

 

 

 

 

Android自己定義了好多ScriptBlock。Android定義的DSL參考文檔在

https://developer.android.com/tools/building/plugin-for-gradle.html下載。注意,它居然沒有提供在線文檔。

圖36所示爲Android的DSL參考信息。

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905194701217?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖36&nbsp; Android Gradle DSL參考示意</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

圖37爲buildToolsVersioncompileSdkVersion的說明:

 

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905194720267?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖37&nbsp; buildToolsVersion和compileSdkVersion的說明</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

從圖37可知,這兩個變量是必須要設置的.....

 

5. CPosDeviceServerApk build.gradle

再來看一個APK的build,它包含NDK的編譯,並且還要簽名。根據項目的需求,我們只能籤debug版的,而release版的簽名得發佈unsigned包給領導簽名。另外,CPosDeviceServerAPK依賴CPosDeviceSdk。

雖然我可以先編譯CPosDeviceSdk,得到對應的jar包,然後設置CPosDeviceServerApk直接依賴這個jar包就好。但是我更希望CPosDeviceServerApk能直接依賴於CPosDeviceSdk這個工程。這樣,整個posdevice可以做到這幾個Project的依賴關係是最新的。

[build.gradle]

 

  1. apply plugin: 'com.android.application' //APK編譯必須加載這個插件
  2. android {
  3. compileSdkVersion gradle.api
  4. buildToolsVersion "22.0.1"
  5. sourceSets{ //差不多的設置
  6. main{
  7. manifest.srcFile 'AndroidManifest.xml'
  8. //通過設置jni目錄爲空,我們可不使用apk插件的jni編譯功能。爲什麼?因爲據說
  9. //APK插件的jni功能好像不是很好使....暈菜
  10. jni.srcDirs = []
  11. jniLibs.srcDir 'libs'
  12. aidl.srcDirs=['src']
  13. java.srcDirs=['src']
  14. res.srcDirs=['res']
  15. }
  16. }//main結束
  17. signingConfigs { //設置簽名信息配置
  18. debug { //如果我們在local.properties設置使用特殊的keystore,則使用它
  19. //下面這些設置,無非是函數調用....請務必閱讀API文檔
  20. if(project.gradle.debugKeystore != null){
  21. storeFile file("file://${project.gradle.debugKeystore}")
  22. storePassword "android"
  23. keyAlias "androiddebugkey"
  24. keyPassword "android"
  25. }
  26. }
  27. }//signingConfigs結束
  28. buildTypes {
  29. debug {
  30. signingConfig signingConfigs.debug
  31. jniDebuggable false
  32. }
  33. }//buildTypes結束
  34. dependencies {
  35. //compile:project函數可指定依賴multi-project中的某個子project
  36. compile project(':CPosDeviceSdk')
  37. compile fileTree(dir: 'libs', include: ['*.jar'])
  38. } //dependices結束
  39. repositories{
  40. flatDir {//flatDir:告訴gradle,編譯中依賴的jar包存儲在dirs指定的目錄
  41. name "minsheng-gradle-local-repository"
  42. dirsgradle.LOCAL_JAR_OUT //LOCAL_JAR_OUT是我存放編譯出來的jar包的位置
  43. }
  44. }//repositories結束
  45. }//android結束
  46. /*
  47. 創建一個Task,類型是Exec,這表明它會執行一個命令。我這裏讓他執行ndk的
  48. ndk-build命令,用於編譯ndk。關於Exec類型的Task,請自行腦補Gradle的API
  49. */
  50. //注意此處創建task的方法,是直接{}喔,那麼它後面的tasks.withType(JavaCompile)
  51. //設置的依賴關係,還有意義嗎?Think!如果你能想明白,gradle掌握也就差不多了
  52. task buildNative(type: Exec, description: 'CompileJNI source via NDK') {
  53. if(project.gradle.ndkDir == null) //看看有沒有指定ndk.dir路徑
  54. println "CANNOT Build NDK"
  55. else{
  56. commandLine "/${project.gradle.ndkDir}/ndk-build",
  57. '-C', file('jni').absolutePath,
  58. '-j', Runtime.runtime.availableProcessors(),
  59. 'all', 'NDK_DEBUG=0'
  60. }
  61. }
  62. tasks.withType(JavaCompile) {
  63. compileTask -> compileTask.dependsOn buildNative
  64. }
  65. ......
  66. //對於APK,除了拷貝APK文件到指定目錄外,我還特意爲它們加上了自動版本命名的功能
  67. tasks.getByName("assemble"){
  68. it.doLast{
  69. println "$project.name: After assemble, jar libs are copied tolocal repository"
  70. project.ext.versionName = android.defaultConfig.versionName
  71. println "\t versionName = $versionName"
  72. copyOutput(false)
  73. }
  74. }

 

 

 

 

 

 

6.  結果展示

在posdevice下執行gradle assemble命令,最終的輸出文件都會拷貝到我指定的目錄,結果如圖38所示:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905194741289?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖38&nbsp; posdevice執行結果</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

圖38所示爲posdevice gradle assemble的執行結果:

 

 

  • library包都編譯release版的,copy到xxx/javaLib目錄下
  • apk編譯debug和release-unsigned版的,copy到apps目錄下
  • 所有產出物都自動從AndroidManifest.xml中提取versionName。

 

4.4.4  實例2

下面這個實例也是來自一個實際的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的內容。代碼如下所示:

[build.gradle]

 

  1. apply plugin: 'com.android.application' //加載APP插件
  2. //加載utils.gradle
  3. apply from:rootProject.getRootDir().getAbsolutePath() + "/utils.gradle"
  4. //buildscript設置android app插件的位置
  5. buildscript {
  6. repositories { jcenter() }
  7. dependencies { classpath 'com.android.tools.build:gradle:1.2.3' }
  8. }
  9. //androidScriptBlock
  10. android {
  11. compileSdkVersion gradle.api
  12. buildToolsVersion "22.0.1"
  13. sourceSets{//源碼設置SB
  14. main{
  15. manifest.srcFile 'AndroidManifest.xml'
  16. jni.srcDirs = []
  17. jniLibs.srcDir 'libs'
  18. aidl.srcDirs=['src']
  19. java.srcDirs=['src']
  20. res.srcDirs=['res']
  21. assets.srcDirs = ['assets'] //多了一個assets目錄
  22. }
  23. }
  24. signingConfigs {//簽名設置
  25. debug { //debug對應的SB。注意
  26. if(project.gradle.debugKeystore != null){
  27. storeFile file("file://${project.gradle.debugKeystore}")
  28. storePassword "android"
  29. keyAlias "androiddebugkey"
  30. keyPassword "android"
  31. }
  32. }
  33. }
  34. /*
  35. 最關鍵的內容來了: buildTypesScriptBlock.
  36. buildTypes和上面的signingConfigs,當我們在build.gradle中通過{}配置它的時候,
  37. 其背後的所代表的對象是NamedDomainObjectContainer<BuildType> 和
  38. NamedDomainObjectContainer<SigningConfig>
  39. 注意,NamedDomainObjectContainer<BuildType/或者SigningConfig>是一種容器,
  40. 容器的元素是BuildType或者SigningConfig。我們在debug{}要填充BuildType或者
  41. SigningConfig所包的元素,比如storePassword就是SigningConfig類的成員。而proguardFile等
  42. 是BuildType的成員。
  43. 那麼,爲什麼要使用NamedDomainObjectContainer這種數據結構呢?因爲往這種容器裏
  44. 添加元素可以採用這樣的方法: 比如signingConfig爲例
  45. signingConfig{//這是一個NamedDomainObjectContainer<SigningConfig>
  46. test1{//新建一個名爲test1的SigningConfig元素,然後添加到容器裏
  47. //在這個花括號中設置SigningConfig的成員變量的值
  48. }
  49. test2{//新建一個名爲test2的SigningConfig元素,然後添加到容器裏
  50. //在這個花括號中設置SigningConfig的成員變量的值
  51. }
  52. }
  53. 在buildTypes中,Android默認爲這幾個NamedDomainObjectContainer添加了
  54. debug和release對應的對象。如果我們再添加別的名字的東西,那麼gradleassemble的時候
  55. 也會編譯這個名字的apk出來。比如,我添加一個名爲test的buildTypes,那麼gradle assemble
  56. 就會編譯一個xxx-test-yy.apk。在此,test就好像debug、release一樣。
  57. */
  58. buildTypes{
  59. debug{ //修改debug的signingConfig爲signingConfig.debug配置
  60. signingConfig signingConfigs.debug
  61. }
  62. demo{ //demo版需要混淆
  63. proguardFile 'proguard-project.txt'
  64. signingConfig signingConfigs.debug
  65. }
  66. //release版沒有設置,所以默認沒有簽名,沒有混淆
  67. }
  68. ......//其他和posdevice 類似的處理。來看如何動態生成runtime_config文件
  69. def runtime_config_file = 'assets/runtime_config'
  70. /*
  71. 我們在gradle解析完整個任務之後,找到對應的Task,然後在裏邊添加一個doFirst Action
  72. 這樣能確保編譯開始的時候,我們就把runtime_config文件準備好了。
  73. 注意,必須在afterEvaluate裏邊才能做,否則gradle沒有建立完任務有向圖,你是找不到
  74. 什麼preDebugBuild之類的任務的
  75. */
  76. project.afterEvaluate{
  77. //找到preDebugBuild任務,然後添加一個Action
  78. tasks.getByName("preDebugBuild"){
  79. it.doFirst{
  80. println "generate debug configuration for ${project.name}"
  81. def configFile = new File(runtime_config_file)
  82. configFile.withOutputStream{os->
  83. os << I am Debug\n' //往配置文件裏寫 I am Debug
  84. }
  85. }
  86. }
  87. //找到preReleaseBuild任務
  88. tasks.getByName("preReleaseBuild"){
  89. it.doFirst{
  90. println "generate release configuration for ${project.name}"
  91. def configFile = new File(runtime_config_file)
  92. configFile.withOutputStream{os->
  93. os << I am release\n'
  94. }
  95. }
  96. }
  97. //找到preDemoBuild。這個任務明顯是因爲我們在buildType裏添加了一個demo的元素
  98. //所以Android APP插件自動爲我們生成的
  99. tasks.getByName("preDemoBuild"){
  100. it.doFirst{
  101. println "generate offlinedemo configuration for${project.name}"
  102. def configFile = new File(runtime_config_file)
  103. configFile.withOutputStream{os->
  104. os << I am Demo\n'
  105. }
  106. }
  107. }
  108. }
  109. }
  110. .....//copyOutput

 

 

 

 

 

 

最終的結果如圖39所示:

 

		<p><img alt="" class="has" src="https://img-blog.csdn.net/20150905200639926?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQv/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center"></p>

		<p>圖39&nbsp; 實例2的結果</p>
		</td>
	</tr></tbody></table></div><p>&nbsp;</p>

幾個問題,爲什麼我知道有preXXXBuild這樣的任務?

 

答案:gradle tasks --all查看所有任務。然後,多嘗試幾次,直到成功

 

五、總結

到此,我個人覺得Gradle相關的內容都講完了。很難相信我僅花了1個小時不到的時間就爲實例2添加了gradle編譯支持。在一週以前,我還覺得這是個心病。回想學習gradle的一個月時間裏,走過不少彎路,求解問題的思路也和最開始不一樣:

 

  • 最開始的時候,我一直把gradle當做腳本看。然後到處到網上找怎麼配置gradle。可能能編譯成功,但是完全不知道爲什麼。比如NameDomainObjectContainer,爲什麼有debug、release。能自己加別的嗎?不知道怎麼加,沒有章法,沒有參考。出了問題只能google,找到一個解法,試一試,成功就不管。這麼搞,心裏不踏實。
  • 另外,對語法不熟悉,尤其是Groovy語法,雖然看了下快速教材,但總感覺一到gradle就看不懂。主要問題還是閉包,比如Groovy那一節寫得文件拷貝的例子中的withOutputStream,還有gradle中的withType,都是些啥玩意啊?
  • 所以後來下決心先把Groovy學會,主要是把自己暴露在閉包裏邊。另外,Groovy是一門語言,總得有SDK說明吧。寫了幾個例子,慢慢體會到Groovy的好處,也熟悉Groovy的語法了。
  • 接着開始看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當做編程框架來看,對於我們這些程序員來說,寫這幾百行代碼,那還算是事嘛??

 

 

 

[原文鏈接](https://blog.csdn.net/innost/article/details/48228651)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章