構建工具的發展及Android Gradle快速上手
前話:
最近谷歌宣布官方不再維護Eclipse ADT了,之后將更加專注于Android Studio的功能和性能上的改進,早在2013年的Google IO大會上首次推出了Android Studio,當時剛出來的時候我就好奇的去下載體驗了一下,想看一下新開發工具的優勢在哪里,據官方介紹,最吸引我的一點就是使用Studio使用了 Gradle編譯系統,可以支持很靈活的定制需求,而我當時正在研究當成庫使用的APK(就是現在的aar文件,不過當時還沒有出身),剛好遇到了ADT 編譯系統的限制,所以當時看到Studio非常興奮,于是我當時就進行過一系列的研究,包括Gradle,Groovy,Aant,Maven,不過當時太懶沒有留下文章只是做了一些筆記。我曾經也試著在自己公司推廣Gradie,但當時同事們還是不太愿意去額外學習一個工具,覺得Eclipse夠用,然后項目組也覺得有風險,所以當時就把這個事放下了,直到今年,Google大力推廣Studio,還把ADT直接從Android官方下了,當前項目組也因為只在Studio所支持的multi-dex特性而被迫遷移了到了Studio,其實當時集體遷,我還是覺得有點風險,不過遷了之后發現并沒想像中那么麻煩,甚至非常簡單,因為可以讓同一份代碼同時支持Eclipse編譯和Gradle編譯,當然,這不是Google官方所建議的,但卻是最受同事歡迎的,這樣可以無縫遷移,而且遷移工作也很簡單,就是在每個Eclipse工程(包括主工程和庫工程)目錄下放一個build.gradle就可以,具體做法,我寫到另外一篇文章中吧(等我閑的時候寫)。因為不得不遷移,所以我寫下這篇文章,希望幫助新上手同學理解Gradle,使大家可以看懂Gradle 構建腳本,并且能定制一些簡單的個性化編譯需求。
構建工具的的展:
大多數介紹gradle的文章都會寫到:Gradle既有Ant的強大和靈活性,又有Maven的易用性。 ant和maven是什么,也許你沒聽過,也許你是那個領域的專家,簡單來說,他們都構建工具,構建是英文build的翻譯,所以,何謂構建工具,如果你一直使用IDE作為開發工具,可能會不太清楚,因為IDE已經幫你把所有的活干了(我不是反對用IDE,而是覺得可以去了解一下IDE的內部流程),構建工具不同于編譯工具,他是用于組織編譯、單元測試、發布等操作,并且簡化這些操作,構建工具與編譯工具的關系是構建工具調用了編譯工具,每當你執行一次構建操作的時候,內部實際自動執行了編譯、單元測試,發布等操作。也許你會說為什么要構建工具,我寫個腳本不就行了,我第一個學習構建工具—— Makefile的時候也是這么想的,如果只是用于組織編譯步驟,寫個腳本確實簡單得多,不過構建工具并不是簡單的調用編譯等操作,他還要提高效率和節省資源,比如當你第二次執行構建時,如果源代碼沒有任何修改,構建工具應該聰明的跳過編譯操作,直接使用上一次的編譯成果,如果你的源代碼只有部分修改,那么構建工具應該僅部分編譯修改過的內容。也許睿智的你會立馬想到,我在腳本里加個If判斷也行啊,你當然可以那樣實現,但隨時著項目規模的擴大,那樣的腳本復雜度會呈指數型上升,直接你的自己都不著維護那么腳本,一旦有新的編譯需要,那將會是你的噩夢。構建工具誕生就是為了優雅解決這些問題,有了構建工具之后,寫一個簡潔的構建腳本,便可以輕松的應對這一切。
在詳細介紹Gradle之前,我們先來細數一下構建工具的發展吧,最初最元老的構建工具當然算Makefile了,Makefile的強大讓他馳騁了幾十年,至今仍是Linux上C/C++開發最流行的構建工具,上G級別的Android系統開源項目就是由Makefile構建的,不但強大,Makefile腳本還很簡單易用,上手快,Makefile腳本就是包含一系列規則,每條規則包含一個目標、依賴和命令,每個目標對應一些依賴和一串命令,一個目標是將命令作用于他的依賴上生成的,比如你用C寫了個helloworld.c,你可以寫一個目標為helloworld,他的依賴是 helloworld.c,他的命令是gcc helloworld.c -o helloworld。即這個簡單Makefile腳本就僅包含一條規則,內容如下:
- helloworld: helloworld.c
- gcc helloworld.c -o helloworld </ol> </div>
- 對象A: 對象B
- 命令...
- 對象B: 對象C
- 命令... </ol> </div> 大項目往往有很多條規則,于是就形成了樹形的依賴鏈,Makefile就遞歸的對比目標和依賴新舊來決定某條鏈是否要重新生成。雖然Makefile不只一種規范,但大同小異,其中以GNU Make最流行。
- <project>
- <target name="compile">
- <mkdir dir="build/classes"/>
- <javac srcdir="src" destdir="build/classes"/>
- </target>
- <target name="jar" depends="compile">
- <mkdir dir="build/jar"/>
- <jar destfile="build/jar/HelloWorld.jar" basedir="build/classes"/>
- </target>
- </project> </ol> </div> 簡單的說,Ant就是一系列target和task,也正因為與Makefile思想的相近性,使得用Ant編譯C項目也是很簡單的,不過沒必要這么干,還要裝個JVM,多麻煩。
- my-app
- |-- pom.xml
- `-- src
- |-- main
- | `-- java
- | `-- com
- | `-- mycompany
- | `-- app
- | `-- App.java
- `-- test
- `-- java
- `-- com
- `-- mycompany
- `-- app
- `-- AppTest.java </ol> </div> 構建配置文件內容如下:
- <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <groupId>com.mycompany.app</groupId>
- <artifactId>my-app</artifactId>
- <version>1.0-SNAPSHOT</version>
- <packaging>jar</packaging>
- </project> </ol> </div> 這兩點在Maven中是POM(Project Object Model)的內容,POM即項目對象模型,是Maven2引入的概念(Maven1已經不用了,不管他了),Maven會為每個項目,根據其構建配置文件,建立一個模型,然后根據這個模型來構建項目;第三,Maven引入了中心庫依賴管理,即開發者可以將自己的庫Jar包上傳到Maven中心倉庫(這個倉庫自己也可以搭建,也可以使用Maven官方的免費倉庫),其它開發者在pom.xml中申明該依賴(填寫地址和版本號),構建的時候,Maven會自動從中心倉庫下載,還可以解析依賴鏈,下載所有對應的庫文件,例如:
- <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <groupId>com.mycompany.app</groupId>
- <artifactId>my-app</artifactId>
- <version>1.0-SNAPSHOT</version>
- <packaging>jar</packaging>
- <dependencies>
- <dependency>
- <groupId>junit</groupId>
- <artifactId>junit</artifactId>
- <version>4.8.2</version>
- <scope>test</scope>
- </dependency>
- </dependencies>
- </project> </ol> </div>
- task hello {
- doLast {
- println 'Hello world!'
- }
- } </ol> </div> 可以看出Gradle就是在做通用語言做的事, 一個編譯Java項目的Gradle如下:
- apply plugin: 'java' </ol> </div> 沒錯,只有一行,和Maven一樣,約定了類似的目錄結構,將會編譯src/main/java下的源代碼。
- buildscript {
- repositories {
- jcenter()
- }
- dependencies {
- classpath 'com.android.tools.build:gradle:1.0.1'
- }
- }
- apply plugin: 'com.android.library'
- android {
- compileSdkVersion 21
- buildToolsVersion '21.1.1'
- }
- dependencies {
- compile fileTree(dir: 'libs', include: '*.jar')
- } </ol> </div> 也許和你以前看到的例子不同,這個是精簡版的,比Studio生成的例子還簡單一點,這一個文件(除了local.properties)就可以編譯一個項目了。
- HelloAndroidGradle
- |-- build.gradle
- |-- settings.gradle
- |-- local.properties
- `-- app
- |-- build.gradle
- `-- libA
- |-- build.gradle </ol> </div> local.properties的作用很簡單,用于存一些本地配置,如Android SDK的路徑,settings.gradle主要是用于指明包含哪些模塊,如:
- include 'app'
- include 'libA' </ol> </div> 如果你發現很多地方有settings.gradle,不用管它,因為只有項目根目錄的settings.gradle才會生效,當然你也可以使用子目錄作為項目的根目錄,如app目錄,這樣可以從app目錄構建項目;根目錄的build.gradle是一些全局的東西,一般包含一個buildscript 的代碼塊,是用于是配置構建工具的,比如構建腳本自身依賴的Android Plugin,如:
- <span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"></span><pre name="code" class="plain">buildscript {
- repositories {<span style="white-space:pre"> </span>// 編譯腳本的倉庫配置,用于搜索腳本本身的依賴庫
- jcenter()
- }
- dependencies {
- classpath 'com.android.tools.build:gradle:1.0.1'<span style="white-space:pre"> </span>// Android Plugin
- }
- }
- allprojects {<span style="white- space:pre"> </span>// 全局倉庫配置,用搜索項目<span style="font- family: Arial, Helvetica, sans-serif;">的依賴庫</span>
- repositories {
- jcenter()
- }
- } </ol> </div> 你的項目本身是不依賴Android Plugin的,這里的依賴庫是不會打包到APK中的;app和libA都是一個模塊,其下的build.gradle用于構建本模塊的。這里的一個模塊相同于Eclipse中的一個工程,如果在Eclipse里,app就會為主工程,libA為庫工程,app依賴libA。app和libA的 build.gradle內容分別如下:
- apply plugin: 'com.android.<span style="color:#993399;">application</span>'
- android {
- compileSdkVersion 21
- buildToolsVersion '21.1.1'
- }
- dependencies {
- compile fileTree(dir: 'libs', include: '*.jar') </ol> </div>
- compile project(':libA') </ol> </div>
- } </ol> </div>
- apply plugin: 'com.android.<span style="color:#993399;">library</span>'
- android {
- compileSdkVersion 21
- buildToolsVersion '21.1.1'
- }
- dependencies {
- compile fileTree(dir: 'libs', include: '*.jar')
- } </ol> </div> 只有一行不同,dependencies語句塊配置了依賴關系,其中fileTree(dir: 'libs', include: '*.jar')是指libs目錄下的所有jar文件,compile project(':libA')是指依賴另一個模塊libA,當然還可以申明存在于中心倉庫的依賴,如:
- compile 'com.android.support:appcompat-v7:21.0.3' </ol> </div> 如果你使用Gradle構建項目,從命令行構建與使用Studio構建是一樣的邏輯,Studio會根據同步build腳本建立IDEA內部的配置,當你修改build腳本時,Studio也會提示你同步,所以建議大家使用build腳本來配置構建需求,而不是使用IDE,以免IDE會調整你的build腳本導致不易讀。
- android {
- ...
- flavorDimensions "abi", "version" // 申明有兩個維度:abi和version
- productFlavors {
- freeapp { // 維度的取值
- flavorDimension "version" // 這個取值屬于名為version的維度
- ...
- }
- x86 { //維度的取值
- flavorDimension "abi" // 這個取值屬于名為abi的維度
- ...
- }
- ...
- }
- } </ol> </div>
構建之后,會生成名為helloworld的可執行文件,每次你執行構建的時候,Makefile會比較helloworld和 helloworld.c,看哪個新,如果helloworld.c前就運行命令"gcc helloworld.c -o helloworld"重新生成helloworld,否則直接結束。不過,真實情況下,往往會有很多目標和依賴,一個目標(對象A)的依賴(對象B)還可能依融另一個對象C,比如你的可執行程序(對象A),依融某庫(對象B),而對象B又靠一個代碼文件(對象C)來生成,這時你就要寫兩條這樣的規則了,大致如下:
Makefile的原理可以讓我們更好的理解更高級的構建工具,所以長篇大論了這么久。Makefile出來之后,有一段很長的統治時期,直到 Java出世,Java是為跨平臺而生,而Makefile成了一個大大的絆腳石,所以Java開發人員急切的需要一個跨平臺的構建工具,最好能在JVM 上運行,于是Ant誕生了,不可否定,Ant有很多思想來自于Makefile,雖然有很多改進。在我看來,Ant構建腳本相比Makefile腳本更簡單了,不過可能要長一點,因為Ant使用了XML文件格式,不過無關緊要,XML文件只是眾多能承載樹形結形的載體格式之一,如果你愿意,可以開發個使用 JSON格式文件的Ant,正因為Ant構建腳本的思想更簡單了,所以很多人更愿意叫他為構建配置文件,把他當成一個用XML呈現的配置文件,腳本一般是指一連串可執行的命令,配置文件一般是指能被程序掃描成結構化的數據體,所以你可以認為Ant執行一個構建腳本來做一次構建,也可以認為Ant掃描了一個配置文件,根據其配置項做了一次構建,都說得過去。Ant腳本名稱為build.xml,一個Ant腳本包括project、target、task、 propert四大元素,其中target跟Makefile的含義基本一樣,每個構建腳本只包一個project,至少一個target,每個target包含若于task,每個task相當于一個命令,如mkdir,在Ant執行階段會實例化ant.jar里的一個 Task的子類,每個target有若干個Attribute,有的是必要的,有的非必要,是執行該命令是需要的參數,propert可以先不管它,相當于一個變量。一個簡單的helloworld的Ant腳本如下:
Ant出現之后,為很多Java開發人員帶來福音,不過隨著軟件行業的日益發展,軟件規模越來越大,大家開始慢慢的發現Ant不夠用,一方面是覺得不同項目存在很多相同的構建流程,但每開一個新項目,不得不重復寫一遍那些流程,比如大部分項目的構建流程都是編譯、單元測試,打包、發布,所以是人們希望構建工具內部將這一部分共同的東西固化下來以便復用,來加速新項目的周期,特別是中小項目;另一方面,人們發現,一個項目往往依賴很多其它項目,其它項目又依賴其它項目,為了構建,人們不得不重復的拷貝這些庫項目,同時開發庫項目的人也不能把最新的版本快速的推廣出去,應用周期很多長。為了解決這一系列問題,Maven出世了,Maven與之前的構建工具有極大的區別,雖然他使用XML文件格式(pom.xml),首先他固化了構建流程,他認為構建過程是基本不變的,在Maven中稱為標準構建生命周期,只是其中的某些步驟需要定制,所以Maven的構建腳本文件更應該稱為構建配置文件,其中的配置項定制了子步驟的一些屬性;其次Maven約定了一套工程目錄結構,假如你使用(也強制建議使用)這套目錄結構,你使用極少的配置就可以構建好你的工程,這個目錄結構大概如下:
正如官方所述,Maven的目的是:1)簡化項目構建;2)建立一個標準的統一的項目構建方法;3)對項目的組成有一個清晰的描述;4)簡化的項目發布,不同項目之間共享jar,即更好支持多項目;5)簡化Java開發者的日常工作。在我看來,Maven是構建工具史上的一次大的重構,敢于標新立異,打破常規,重新思考并從頭設計。Maven推出后,由于之前積累了太多優秀的Ant構建的項目,所以Apache給Ant加了一對翅膀——Ivy,幫 Ant實現中心倉庫依賴管理,Ivy跟Ant的風格一致,相比Maven,有更加十分靈活的配置。
Gradle介紹:
2004年,Maven發布后(實際上2002就已作為Apache Turbine的子工程存在),非常受Java開發人員的愛戴。然而,軟件史上,再偉大的項目都有他的喪失光芒的那一天,不過舊項目的過時往往是另一個更強大的新項目的誕生,Gradle就是Maven的后生來者,在他的問世之初就受到各大開源社區和業界的好評,在短時間內就收到了極大的關注,也在那時,Google迅速將Android的編譯環境遷移到了Gradle,當你搜索Gradle時,你應該會有這樣的感覺,為何人們都如此愛好這個工具,其中很大一部分原因是 Gradle使用了基于Groovy的DSL語言作為構建腳本語言的而不是以往的XML,這里應該會迅速產生兩個疑問:1.Groovy是什么;2.DSL是什么。
首次Groovy是一種編程語言,在我看來,Groovy是能運行在Java虛擬機上的“Python”,當然他不是Python,況且也存在真正能夠運行在JVM上的JPython,那我為什么稱之為運行在JVM上的Python呢,在《Groovy In Action》的前言中提到,Groovy的作者非常喜歡Python,但出于一些限制,所以他創造了一種能夠在JVM運行的類似的語言,在Groovy 里,極大的借鑒了Python里的基本數據結構及語法,使得很簡潔的代碼就可以在Java虛擬機里運行,大家都知道解析型語言的特點就是語法簡潔,很短的代碼可以做很多事,而且免編譯,調試非常方便,還可以交互式編程,像敲命令一樣,很多時候加入解析型語言混合編程可以極大的提高效率,例如使用解析型語言寫單元測試非常快,寫小助手或小工具也十常快,在Java項目中,使用Java和Groovy混合編程是很簡單的事,他們可以相互調用,所以我很想將這種混合編程的方式引入Android開發中,以便使用Groovy寫單元測試以及用Groovy作為插件代碼,在線下載運行,都很方便,不過我至今還在實驗階段。
DSL就簡單了,DSL即Domain Special Language,領域專用語言,別管他名字這么高深,其它含義很簡單,領域專用即只用于某個領域的語言,與之對應的是通用語言,例如,Java和C就是通用語言,而Makefile構建腳本里的就是領域專用語言,這種語言有簡單的語法,但只能用于構建項目,沒人能用Makefile語言開發一個游戲吧。那什么是基于Groovy的DSL語言,這要說Groovy的別一個偉大的特性了,即對領域語言的強大支持性,之所以有這么強大的支持性,是因為 Groovy內建強大的操作元數據(Meta-data)的能力(這也是 Groovy優勢,且是Java的短板),元數據又得解釋一下,簡單說元數據就是一些內在屬性,比如你有一個對象,那這個對象的類,這個對象的創建時間,就是他的元數據,又比如一個類,這個類的成員列表,方法列表就是這個類的元數據,一般情況下我們不會用到這些元數據,但想在這個語言基礎構建另一門DSL 語言就必須訪問和操作元數據,Groovy構建DSL語言的原理大概如下,你可以注冊一個監聽器,當Groovy代碼運行的時候,有方法調用的時候會通知你,有參數傳入的時候會通知你,創建對錫的時候會通知你,有點像AOP,你收到這些通知可以做什么多事,比如你隨便寫一段代碼放到Groovy文件里,是不遵循Groovy語法的,這時,Groovy系統就會通知你有一段這樣的代碼來了,而你可以根據這段代碼做任何事,比如你收到一個@號,就做勾股定律運算(即對x平方+y平方的和求平方根),收到一個#號就求圓周長運算,然后把執行結果返回給Groovy系統,這樣人家寫一個int a = 3@4; // a 將等于5 這樣代碼就是你剛剛創造的DSL語言,你可以運行起來,還可以給他取名為NiuBi語言。
這樣解釋,大家應該明白Gradle為什么這么強大了吧,別人用的是XML文件,而Gradle用是編程語言,雖然Gradle里是用了基于 Groovy的DSL語言,但符合Groovy語法的語句大多可以在Gradle腳本中直接運行,這樣相當于Gradle腳本有了通用語言的功能,這樣你有個性化定制需求的時候,就可以使用你平時編程同樣的思路去實現,而DSL的特性又簡化了常用構建需求的實現,于是即有靈活和強大的擴展性,又有易用性,其實是結合了通用語言和DSL語言的優點。其實這也是軟件發展的必然結果,隨著軟件的發展,軟件復雜度肯定是越來越復雜,人們對構建的需求肯定會越來越多,以至于今天的構建需求的復雜度達到了之前普通程序的復雜度,以至于構建需求也需要通用語言才能滿足,而DSL語言只為了簡化常用需求的實現,增加便捷性。
Gradle即靈活又易用還有其它原因,一個重要的原因是,Gradle里引入了插件的思想,對于常用的構建需求都通過了插件來簡化,比如Java 插件,當你應用它時,構建Java的時候就跟Maven一樣簡單快捷,這個思想很巧妙,Gradle腳本具備通用語言的靈活性,同時將那些常用的構建任務封裝成插件,用于簡化常用任務,使得Gradle即強大又快捷。我們還可以自己寫插件,雖然Maven也有類似的擴展,但沒有Gradle方便,原因還在 Groovy,因為可以用Groovy語言寫插件,代碼可以很簡短,同時不用編譯,Maven的擴展是用Java寫了之后編譯的。
此外,在依賴管理方面,Gradle最先使用了Ivy,后來自己開發了一個全新的依賴管理模塊,跟Ivy不同,能同時對接Maven和Ivy的中心倉庫。Gradle的Java插件也約定了和Maven一樣類似的目錄結構。
下面介紹一下Gradle腳本的幾個概念,Gradle兩個核心概念:Project和Task,這與Ant類似。每個腳本包含一個或多個 Project,每個Project由一個或多個Task,一個Task包含一個或多個Action,一個Action就是一個代碼塊,代碼塊就是一些跟通用語言一樣的語句,Gradle還可以使用Ant Task。一個簡單的Gradle腳本如下:
使用Gradle構建Android:
Android使用Gradle做為構建工具,其實只是Google做了一個Gradle Android Plugin,在Gradle腳本中應用Android Plugin之后,就可以很方便的構建Android項目了,本文內容只對構建結構介紹,希望大家能看懂腳本,并能添加簡單的功能,并不包含step by step教程,因為這種類型的好文章太多了,再多寫也沒意義,一個簡單的例子如下:
不過,通常情況下,一個項目的結構稍復雜點,一般項目根目錄有一個build.gradle和settings.gradle,而根目錄下包括若干個模塊,每個模塊下都有一個build.gradle。如:
與
擴展:
能在Gradle腳本中配置的項目太多,本文不打算一一例舉,請大家參考Gradle Plugin User Guide,其中有幾個Android Plugin新增的概念不太好理解,我在此做一下解釋,未見過下面東西的同學無視之。
1)Build Type:構建類型,包括release和debug;
2)Product Flavor:產品風味(不好翻譯),用于創建不同特性的產出物,如免費版和付費版;
3)Build Variant:構建變種(中文翻譯真難聽),Build Type + Product Flavor = Build Variant,以上兩個元素不同的組合就產出不同的變種,如免費版的debug版。
4)Flavor Dimensions:風味維度,用于創建出復合產品風味,這種產品風味是由多個風味維度組合出來的,例如:一個項目的發布的版本一方面可以從處理器架構來分為arm、x86,mips,另一方面又可以分為免費版和付費版,所以最終的產品風味肯定是這兩個維度的組合,如arm免費版、arm付費版、x86 免費版、x86付費版、mips免費版,mips收費版。當你的產品風味很多的時候,比如大于3個維度,每個維度的取值還很多,就可以使用這種復合產品風格,來簡化build腳本。注意,使用風味維度時,寫法有點奇怪,是用逆向思維,申明維度之后,先寫出維度的取值,再寫出這個取值屬于哪個維度,如:
結束語:
參考資料:
· Java Build Tools: Ant vs Maven vs Gradle
· 《Groovy In Action》
· DSL Wiki
來自:http://blog.csdn.net/yanquan345/article/details/46710869