構建工具的發展及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

構建之後,會生成名爲helloworld的可執行文件,每次你執行構建的時候,Makefile會比較helloworld和helloworld.c,看哪個新,如果helloworld.c新就運行命令"gcc helloworld.c -o helloworld"重新生成helloworld,否則直接結束。不過,真實情況下,往往會有很多目標和依賴,一個目標(對象A)的依賴(對象B)還可能依融另一個對象C,比如你的可執行程序(對象A),依融某庫(對象B),而對象B又靠一個代碼文件(對象C)來生成,這時你就要寫兩條這樣的規則了,大致如下:

對象A: 對象B
    命令...
對象B: 對象C
    命令...
大項目往往有很多條規則,於是就形成了樹形的依賴鏈,Makefile就遞歸的對比目標和依賴新舊來決定某條鏈是否要重新生成。雖然Makefile不只一種規範,但大同小異,其中以GNU Make最流行。

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腳本如下:

<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>
簡單的說,Ant就是一系列target和task,也正因爲與Makefile思想的相近性,使得用Ant編譯C項目也是很簡單的,不過沒必要這麼幹,還要裝個JVM,多麻煩。

Ant出現之後,爲很多Java開發人員帶來福音,不過隨着軟件行業的日益發展,軟件規模越來越大,大家開始慢慢的發現Ant不夠用,一方面是覺得不同項目存在很多相同的構建流程,但每開一個新項目,不得不重複寫一遍那些流程,比如大部分項目的構建流程都是編譯、單元測試,打包、發佈,所以是人們希望構建工具內部將這一部分共同的東西固化下來以便複用,來加速新項目的週期,特別是中小項目;另一方面,人們發現,一個項目往往依賴很多其它項目,其它項目又依賴其它項目,爲了構建,人們不得不重複的拷貝這些庫項目,同時開發庫項目的人也不能把最新的版本快速的推廣出去,應用週期很多長。爲了解決這一系列問題,Maven出世了,Maven與之前的構建工具有極大的區別,雖然他使用XML文件格式(pom.xml),首先他固化了構建流程,他認爲構建過程是基本不變的,在Maven中稱爲標準構建生命週期,只是其中的某些步驟需要定製,所以Maven的構建腳本文件更應該稱爲構建配置文件,其中的配置項定製了子步驟的一些屬性;其次Maven約定了一套工程目錄結構,假如你使用(也強制建議使用)這套目錄結構,你使用極少的配置就可以構建好你的工程,這個目錄結構大概如下:

my-app
|-- pom.xml
`-- src
    |-- main
    |   `-- java
    |       `-- com
    |           `-- mycompany
    |               `-- app
    |                   `-- App.java
    `-- test
        `-- java
            `-- com
                `-- mycompany
                    `-- app
                        `-- AppTest.java
構建配置文件內容如下:
<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>
這兩點在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>

正如官方所述,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腳本如下:

task hello {
    doLast {
        println 'Hello world!'
    }
}
可以看出Gradle就是在做通用語言做的事, 一個編譯Java項目的Gradle如下:
apply plugin: 'java'
沒錯,只有一行,和Maven一樣,約定了類似的目錄結構,將會編譯src/main/java下的源代碼。


使用Gradle構建Android:

Android使用Gradle做爲構建工具,其實只是Google做了一個Gradle Android Plugin,在Gradle腳本中應用Android Plugin之後,就可以很方便的構建Android項目了,本文內容只對構建結構介紹,希望大家能看懂腳本,並能添加簡單的功能,並不包含step by step教程,因爲這種類型的好文章太多了,再多寫也沒意義,一個簡單的例子如下:

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')
}
也許和你以前看到的例子不同,這個是精簡版的,比Studio生成的例子還簡單一點,這一個文件(除了local.properties)就可以編譯一個項目了。

不過,通常情況下,一個項目的結構稍複雜點,一般項目根目錄有一個build.gradle和settings.gradle,而根目錄下包括若干個模塊,每個模塊下都有一個build.gradle。如:

HelloAndroidGradle
|-- build.gradle
|-- settings.gradle
|-- local.properties
`-- app
    |-- build.gradle
`-- libA
    |-- build.gradle
local.properties的作用很簡單,用於存一些本地配置,如Android SDK的路徑,settings.gradle主要是用於指明包含哪些模塊,如:
include 'app'
include 'libA'
如果你發現很多地方有settings.gradle,不用管它,因爲只有項目根目錄的settings.gradle纔會生效,當然你也可以使用子目錄作爲項目的根目錄,如app目錄,這樣可以從app目錄構建項目;根目錄的build.gradle是一些全局的東西,一般包含一個buildscript的代碼塊,是用於是配置構建工具的,比如構建腳本自身依賴的Android Plugin,如:
buildscript {
    repositories {  // 編譯腳本的倉庫配置,用於搜索腳本本身的依賴庫
        jcenter()
    }

    dependencies {
        classpath 'com.android.tools.build:gradle:1.0.1' // Android Plugin
    }
}

allprojects {    // 全局倉庫配置,用搜索項目的依賴庫
    repositories {
        jcenter()
    }
}
你的項目本身是不依賴Android Plugin的,這裏的依賴庫是不會打包到APK中的;app和libA都是一個模塊,其下的build.gradle用於構建本模塊的。這裏的一個模塊相同於Eclipse中的一個工程,如果在Eclipse裏,app就會爲主工程,libA爲庫工程,app依賴libA。app和libA的build.gradle內容分別如下:
apply plugin: 'com.android.application'

android {
    compileSdkVersion 21
    buildToolsVersion '21.1.1'
}

dependencies {
    compile fileTree(dir: 'libs', include: '*.jar')
    compile project(':libA')
}

apply plugin: 'com.android.library'


android {
    compileSdkVersion 21
    buildToolsVersion '21.1.1'
}

dependencies {
    compile fileTree(dir: 'libs', include: '*.jar')
}
只有一行不同,dependencies語句塊配置了依賴關係,其中fileTree(dir: 'libs', include: '*.jar')是指libs目錄下的所有jar文件,compile project(':libA')是指依賴另一個模塊libA,當然還可以申明存在於中心倉庫的依賴,如:
compile 'com.android.support:appcompat-v7:21.0.3'
如果你使用Gradle構建項目,從命令行構建與使用Studio構建是一樣的邏輯,Studio會根據同步build腳本建立IDEA內部的配置,當你修改build腳本時,Studio也會提示你同步,所以建議大家使用build腳本來配置構建需求,而不是使用IDE,以免IDE會調整你的build腳本導致不易讀。


擴展:

能在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腳本。注意,使用風味維度時,寫法有點奇怪,是用逆向思維,申明維度之後,先寫出維度的取值,再寫出這個取值屬於哪個維度,如:

android {
    ...

    flavorDimensions "abi", "version"	// 申明有兩個維度:abi和version

    productFlavors {
        freeapp {	// 維度的取值
            flavorDimension "version"	// 這個取值屬於名爲version的維度
            ...
        }

        x86 {	//維度的取值
            flavorDimension "abi"	// 這個取值屬於名爲abi的維度
            ...
        }
		
	...
    }
}

結束語:

Gradle的強大遠遠不止一篇文章所能描述的,希望本文的描述可以提起大家對Gradle的興趣,並進行深入的研究,多做試驗,相信Gradle的魅力會讓你喜歡上他的,也歡迎大家對本文的不足進行批評指點。


參考資料:

· Apache Maven Wiki

· Apache Ivy Wiki

· GNU Make manual

· Maven Document

· Apache Ant Manual

· Java Build Tools: Ant vs Maven vs Gradle

· Gradle User Guide

· Gradle Wiki

· 《Groovy In Action》

· DSL Wiki

· Groovy Document

· Android Plug-in for Gradle

· Gradle Plugin User Guide

· Android New Build System

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章