前言
為什么需要學Gradle?
Gradle 是 Android 現在主流的編譯工具,雖然在Gradle 出現之前和之后都有對應更快的編譯工具出現,但是 Gradle 的優勢就在于它是親兒子,Gradle 確實比較慢,這和它的編譯過程有關,但是現在的Gradle 編譯速度已經有了成倍提高。除此之外,相對其他編譯工具,最重要的,他和 Android Studio 的關系非常緊密,可以說對于一些簡單的程序我們幾乎不需要任何代碼上的配置只使用 Android Studio 就可以完成編譯和運行。
但是對于一些比較復雜的,特別是多人團隊合作的項目我們會需要一些個性化的配置來提高我們的開發效率。比如我們要自定義編譯出的apk包的名字、對于一些特殊產品我們可能會要用同一個項目編譯出免費版和付費版的apk。這些高級的功能都需要我們對配置代碼進行自定義地修改。
最近伴隨著 Android Studio2.0的發布, Gradle 也進行了一次非常大的升級,叫Instant Run.它的編譯速度網上有人用逆天兩個字來形容。當我們第一次點擊run、debug按鈕的時候,它運行時間和我們往常一樣。但是接下去的時間里,你每次修改代碼后點擊run、debug按鈕,對應的改變將迅速的部署到你正在運行的程序上,傳說速度快到你都來不及把注意力集中到手機屏幕上,它就已經做好相應的更改。但是剛出來的似乎對一些項目的兼容性不太好,現在升級后不知道怎么樣。
為什么要了解命令行編譯?
在很多情況下我們都是使用的 Android Studio 來build、debug項目。Android Studio 能滿足我們開發的大多數需求,但是某些情況下命令行能夠讓我們編譯的效率更高,過程更明朗,一些高級的配置也需要熟悉命令行才能夠使用,比如在服務器編譯,某些項目初始化的時候如果直接交給Android Studio ,它會一直Loading,你都不知道它在干嘛,但是用命令行你就知道它卡在了哪個環節,你只需要修改某些代碼,馬上就能夠編譯過了。
了解 Gradle 之后我們可以做什么?
we can do everything what we want.
自定義編譯輸出文件格式。
hook Android 編譯過程。
配置和改善 Gradle 編譯速度
Gralde Overview
History
我們知道,Android 的編譯過程非常復雜:
我們需要一種工具幫我們更快更方便更簡潔地完成 Android 程序的編譯。現在結合Android Studio 我們一般使用的工具都是Gradle, 在 Gradle 出現以前Android 也有對應的編譯工具叫Ant,在Gradle 出現之后,也有新的編譯工具出現,就是FaceBook 的Buck工具。這些編譯工具在出現的時候幾乎都比 Gradle 要快,Gradle 之所以慢是跟它的編譯周期有很大關系。
Gradle 的編譯周期
在解析 Gradle 的編譯過程之前我們需要理解在 Gradle 中非常重要的兩個對象。Project和Task。
每個項目的編譯至少有一個 Project,一個build.gradle就代表一個project,每個project里面包含了多個task,task 里面又包含很多action,action是一個代碼塊,里面包含了需要被執行的代碼。
在編譯過程中, Gradle 會根據build 相關文件,聚合所有的project和task,執行task 中的 action。因為build.gradle文件中的task非常多,先執行哪個后執行那個需要一種邏輯來保證。這種邏輯就是依賴邏輯,幾乎所有的Task 都需要依賴其他 task 來執行,沒有被依賴的task 會首先被執行。所以到最后所有的 Task 會構成一個有向無環圖(DAG Directed Acyclic Graph)的數據結構。
編譯過程分為三個階段:
初始化階段:創建 Project 對象,如果有多個build.gradle,也會創建多個project.
配置階段:在這個階段,會執行所有的編譯腳本,同時還會創建project的所有的task,為后一個階段做準備。
執行階段:在這個階段,gradle 會根據傳入的參數決定如何執行這些task,真正action的執行代碼就在這里.
剛剛我們提到Gradle 編譯的時候的一些相關文件,下面我們挨個解析一下這些文件。
Gradle Files
對于一個gradle 項目,最基礎的文件配置如下:
一個項目有一個setting.gradle、包括一個頂層的build.gradle文件、每個Module 都有自己的一個build.gradle文件。
setting.gradle:這個 setting 文件定義了哪些module 應該被加入到編譯過程,對于單個module 的項目可以不用需要這個文件,但是對于 multimodule 的項目我們就需要這個文件,否則gradle 不知道要加載哪些項目。這個文件的代碼在初始化階段就會被執行。
頂層的build.gradle:頂層的build.gradle文件的配置最終會被應用到所有項目中。它典型的配置如下:
buildscript {? ? repositories {? ? ? ? jcenter()? ? }? ? dependencies {? ? ? ? classpath'com.android.tools.build:gradle:1.2.3'}}allprojects{? ? repositories{? ? ? ? jcenter()? ? }}
buildscript:定義了 Android 編譯工具的類路徑。repositories中,jCenter是一個著名的 Maven 倉庫。
allprojects:中定義的屬性會被應用到所有 moudle 中,但是為了保證每個項目的獨立性,我們一般不會在這里面操作太多共有的東西。
每個項目單獨的 build.gradle:針對每個moudle 的配置,如果這里的定義的選項和頂層build.gradle定義的相同,后者會被覆蓋。典型的 配置內容如下:
apply plugin:第一行代碼應用了Android 程序的gradle插件,作為Android 的應用程序,這一步是必須的,因為plugin中提供了Android 編譯、測試、打包等等的所有task。
android:這是編譯文件中最大的代碼塊,關于android 的所有特殊配置都在這里,這就是又我們前面的聲明的 plugin 提供的。
defaultConfig就是程序的默認配置,注意,如果在AndroidMainfest.xml里面定義了與這里相同的屬性,會以這里的為主。
這里最有必要要說明的是applicationId的選項:在我們曾經定義的AndroidManifest.xml中,那里定義的包名有兩個用途:一個是作為程序的唯一識別ID,防止在同一手機裝兩個一樣的程序;另一個就是作為我們R資源類的包名。在以前我們修改這個ID會導致所有用引用R資源類的地方都要修改。但是現在我們如果修改applicationId只會修改當前程序的ID,而不會去修改源碼中資源文件的引用。
buildTypes:定義了編譯類型,針對每個類型我們可以有不同的編譯配置,不同的編譯配置對應的有不同的編譯命令。默認的有debug、release 的類型。
dependencies:是屬于gradle 的依賴配置。它定義了當前項目需要依賴的其他庫。
Gradle Wrapper
Gradle 不斷的在發展,新的版本難免會對以往的項目有一些向后兼容性的問題,這個時候,gradle wrapper就應運而生了。
gradlw wrapper 包含一些腳本文件和針對不同系統下面的運行文件。wrapper 有版本區分,但是并不需要你手動去下載,當你運行腳本的時候,如果本地沒有會自動下載對應版本文件。
在不同操作系統下面執行的腳本不同,在 Mac 系統下執行./gradlew ...,在windows 下執行gradle.bat進行編譯。
如果你是直接從eclipse 中的項目轉換過來的,程序并不會自動創建wrapper腳本,我們需要手動創建。在命令行輸入以下命令即可
gradlewrapper--gradle-version2.4
它會創建如下目錄結構:
wrapper 就是我們使用命令行編譯的開始。下面我們看看 wrapper 有什么樣的作用。
Gradle basics
Gradle 會根據build 文件的配置生成不同的task,我們可以直接單獨執行每一個task。通過./gradlew tasks列出所有task。如果通過同時還想列出每個task 對應依賴的其他task,可以使用./gradlew tasks -all。
其實每當我們在Android Studio點擊 build,rebuild,clean菜單的時候,執行的就是一些gradle task.
Android tasks
有四個基本的 task, Android 繼承他們分別進行了自己的實現:
assemble:對所有的 buildType 生成 apk 包。
clean:移除所有的編譯輸出文件,比如apk
check:執行lint檢測編譯。
build:同時執行assemble和check命令
這些都是基本的命令,在實際項目中會根據不同的配置,會對這些task 設置不同的依賴。比如 默認的 assmeble 會依賴 assembleDebug 和assembleRelease,如果直接執行assmeble,最后會編譯debug,和release 的所有版本出來。如果我們只需要編譯debug 版本,我們可以運行assembleDebug。
除此之外還有一些常用的新增的其他命令,比如 install命令,會將編譯后的apk 安裝到連接的設備。
我們運行的許多命令除了會輸出到命令行,還會在build文件夾下生產一份運行報告。比如check命令會生成lint-results.html.在build/outputs中。
Configuration
BuildConfig
這個類相信大家都不會陌生,我們最常用的用法就是通過BuildConfig.DEBUG來判斷當前的版本是否是debug版本,如果是就會輸出一些只有在 debug 環境下才會執行的操作。 這個類就是由gradle 根據 配置文件生成的。為什么gradle 可以直接生成一個Java 字節碼類,這就得益于我們的 gradle 的編寫語言是Groovy, Groovy 是一種 JVM 語言,JVM 語言的特征就是,雖然編寫的語法不一樣,但是他們最終都會編程 JVM 字節碼文件。同是JVM 語言的還有 Scala,Kotlin 等等。
這個功能非常強大,我們可以通過在這里設置一些key-value對,這些key-value 對在不同編譯類型的 apk 下的值不同,比如我們可以為debug 和release 兩種環境定義不同的服務器。比如:
除此之外,我們還可以為不同的編譯類型的設置不同的資源文件,比如:
Repositories
Repositories 就是代碼倉庫,這個相信大家都知道,我們平時的添加的一些 dependency 就是從這里下載的,Gradle 支持三種類型的倉庫:Maven,Ivy和一些靜態文件或者文件夾。在編譯的執行階段,gradle 將會從倉庫中取出對應需要的依賴文件,當然,gradle 本地也會有自己的緩存,不會每次都去取這些依賴。
gradle 支持多種 Maven 倉庫,一般我們就是用共有的jCenter就可以了。
有一些項目,可能是一些公司私有的倉庫中的,這時候我們需要手動加入倉庫連接:
如果倉庫有密碼,也可以同時傳入用戶名和密碼
我們也可以使用相對路徑配置本地倉庫,我們可以通過配置項目中存在的靜態文件夾作為本地倉庫:
Dependencies
我們在引用庫的時候,每個庫名稱包含三個元素:組名:庫名稱:版本號,如下:
如果我們要保證我們依賴的庫始終處于最新狀態,我們可以通過添加通配符的方式,比如:
但是我們一般不要這么做,這樣做除了每次編譯都要去做網絡請求查看是否有新版本導致編譯過慢外,最大的弊病在于我們使用過的版本很很困難是測試版,性能得不到保證,所以,在我們引用庫的時候一定要指名依賴版本。
Local dependencies
File dependencies
通過files()方法可以添加文件依賴,如果有很多jar文件,我們也可以通過fileTree()方法添加一個文件夾,除此之外,我們還可以通過通配符的方式添加,如下:
Native libraries
配置本地.so庫。在配置文件中做如下配置,然后在對應位置建立文件夾,加入對應平臺的.so文件。
文件結構如下:
Library projects
如果我們要寫一個library項目讓其他的項目引用,我們的bubild.gradle的plugin 就不能是andrid plugin了,需要引用如下plugin
applyplugin:'com.android.library'
引用的時候在setting文件中include即可。
如果我們不方便直接引用項目,需要通過文件的形式引用,我們也可以將項目打包成aar文件,注意,這種情況下,我們在項目下面新建arrs文件夾,并在build.gradle 文件中配置 倉庫:
當需要引用里面的某個項目時,通過如下方式引用:
Build Variants
在開發中我們可能會有這樣的需求:
我們需要在debug 和 release 兩種情況下配置不同的服務器地址;
當打市場渠道包的時候,我們可能需要打免費版、收費版,或者內部版、外部版的程序。
渠道首發包通常需要要求在歡迎頁添加渠道的logo。等等
為了讓市場版和debug版同時存在與一個手機,我們需要編譯的時候自動給debug版本不一樣的包名。
這些需求都需要在編譯的時候動態根據當前的編譯類型輸出不同樣式的apk文件。這時候就是我們的buildType大展身手的時候了。
Build Type
android 默認的帶有Debug和Release兩種編譯類型。比如我們現在有一個新的statging的編譯類型
Source sets
每當創建一個新的build type 的時候,gradle 默認都會創建一個新的source set。我們可以建立與main文件夾同級的文件夾,根據編譯類型的不同我們可以選擇對某些源碼直接進行替換。
除了代碼可以替換,我們的資源文件也可以替換
除此之外,不同編譯類型的項目,我們的依賴都可以不同,比如,如果我需要在staging和debug兩個版本中使用不同的log框架,我們這樣配置:
Product flavors
前面我們都是針對同一份源碼編譯同一個程序的不同類型,如果我們需要針對同一份源碼編譯不同的程序(包名也不同),比如 免費版和收費版。我們就需要Product flavors。
注意,Product flavors和Build Type是不一樣的,而且他們的屬性也不一樣。所有的 product flavor 版本和defaultConfig 共享所有屬性!
像Build type 一樣,product flavor 也可以有自己的source set文件夾。除此之外,product flavor 和 build type 可以結合,他們的文件夾里面的文件優先級甚至高于 單獨的built type 和product flavor 文件夾的優先級。如果你想對于 blue類型的release 版本有不同的圖標,我們可以建立一個文件夾叫blueRelease,注意,這個順序不能錯,一定是 flavor+buildType 的形式。
更復雜的情況下,我們可能需要多個product 的維度進行組合,比如我想要 color 和 price 兩個維度去構建程序。這時候我們就需要使用flavorDimensions:
根據我們的配置,再次查看我們的task,發現多了這些task:
Resource merge priority
在Build Type中定義的資源優先級最大,在Library 中定義的資源優先級最低。
Signing configurations
如果我們打包市場版的時候,我們需要輸入我們的keystore數據。如果是debug 版本,系統默認會幫我們配置這些信息。這些信息在gradle 中都配置在signingConfigs中。
配置之后我們需要在build type中直接使用
Optimize
Speeding up multimodule builds
可以通過以下方式加快gradle 的編譯:
開啟并行編譯:在項目根目錄下面的gradle.properties中設置
org.gradle.parallel=true
開啟編譯守護進程:該進程在第一次啟動后回一直存在,當你進行二次編譯的時候,可以重用該進程。同樣是在gradle.properties中設置。
org.gradle.daemon=true
org.gradle.jvmargs=-Xms256m -Xmx1024m
Reducing apk file
在編譯的時候,我們可能會有很多資源并沒有用到,此時就可以通過shrinkResources來優化我們的資源文件,除去那些不必要的資源。
如果我們需要查看該命令幫我們減少了多少無用的資源,我們也可以通過運行shrinkReleaseResources命令來查看log.
某些情況下,一些資源是需要通過動態加載的方式載入的,這時候我也需要像 Progard 一樣對我們的資源進行keep操作。方法就是在res/raw/下建立一個keep.xml文件,通過如下方式 keep 資源:
Manual shrinking
對一些特殊的文件或者文件夾,比如 國際化的資源文件、屏幕適配資源,如果我們已經確定了某種型號,而不需要重新適配,我們可以直接去掉不可能會被適配的資源。這在為廠商適配機型定制app的時候是很用的。做法如下:
比如我們可能有非常多的國際化的資源,如果我們應用場景只用到了English,Danish,Dutch的資源,我們可以直接指定我們的resConfig:
對于尺寸文件我們也可以這樣做
Profiling
當我們執行所有task的時候我們都可以通過添加--profile參數生成一份執行報告在reports/profile中。示例如下:
我們可以通過這份報告看出哪個項目耗費的時間最多,哪個環節耗費的時間最多。
Practice
在開發的過程中,我們可能會遇到很多情況需要我們能夠自己定義task,在自定義task 之前,我們先簡單看看groovy 的語法。
Groovy
我們前面看到的那些build.gradle 配置文件,和xml 等的配置文件不同,這些文件可以說就是可以執行的代碼,只是他們的結構看起來通俗易懂,和配置文件沒什么兩樣,這也是Google 之所以選擇Groovy 的原因。除此之外,Groovy 是一門JVM 語言,也就是,Groovy 的代碼最終也會被編譯成JVM 字節碼,交給虛擬機去執行,我們也可以直接反編譯這些字節碼文件。
我們這里簡單地說一下 groovy 一些語法。
變量
在groovy 中,沒有固定的類型,變量可以通過def關鍵字引用,比如:
defname='Andy'
我們通過單引號引用一串字符串的時候這個字符串只是單純的字符串,但是如果使用雙引號引用,在字符串里面還支持插值操作,
defname='Andy'defgreeting="Hello, $name!"
方法
類似 python 一樣,通過def關鍵字定義一個方法。方法如果不指定返回值,默認返回最后一行代碼的值。
defsquare(defnum){? ? num * num}square4
類
Groovy 也是通過Groovy 定義一個類:
classMyGroovyClass{StringgreetingStringgetGreeting() {return'Hello!'}}
在Groovy 中,默認所有的類和方法都是pulic的,所有類的字段都是private的;
和java一樣,我們通過new關鍵字得到類的實例,使用def接受對象的引用:def instance = new MyGroovyClass()
而且在類中聲明的字段都默認會生成對應的setter,getter方法。所以上面的代碼我們可以直接調用instance.setGreeting 'Hello, Groovy!',注意,groovy 的方法調用是可以沒有括號的,而且也不需要分號結尾。除此之外,我們甚至也可以直接調用;
我們可以直接通過instance.greeting這樣的方式拿到字段值,但其實這也會通過其get方法,而且不是直接拿到這個值。
map、collections
在 Groovy 中,定義一個列表是這樣的:
Listlist= [1,2,3,4,5]
遍歷一個列表是這樣的:
list.each() { element ->printlnelement}
定義一個 map 是這樣的:
MappizzaPrices = [margherita:10, pepperoni:12]
獲取一個map 值是這樣的:
pizzaPrices.get('pepperoni')pizzaPrices['pepperoni']
閉包
在Groovy 中有一個閉包的概念。閉包可以理解為就是 Java 中的匿名內部類。閉包支持類似lamda形式的語法調用。如下:
def square = {num->num*num}square8
如果只有一個參數,我們甚至可以省略這個參數,默認使用it作為參數,最后代碼是這樣的:
Closuresquare = {it* it}square16
理解閉包的語法后,我們會發現,其實在我們之前的配置文件里,android,dependencies這些后面緊跟的代碼塊,都是一個閉包而已。
Groovy in Gradle
了解完 groovy 的基本語法后,我們來看看 gradle 里面的代碼就好理解多了。
apply
applyplugin:'com.android.application'
這段代碼其實就是調用了project對象的apply方法,傳入了一個以plugin為key的map。完整寫出來就是這樣的:
project.apply([plugin:'com.android.application'])
實際調用的時候會傳入一個DependencyHandler的閉包,代碼如下:
Task
運行該 task
./gradlew hello
注意:我們前面說過,gradle的生命周期分三步,初始化,配置和執行。上面的代碼在配置過程就已經執行了,所以,打印出的字符串發生在該任務執行之前,如果要在執行階段才執行任務中的代碼應該如下設置:
添加Action:前面我們說過task 包含系列的action,當task 被執行的時候,所有的action 都會被依次執行。如果我們要加入自己的action,我們可以通過復寫doFirst()和doLast()方法。
打印出來是這樣的:
Task 依賴:前面我們也說過,task 之間的關系就是依賴關系,關于Task 的依賴有兩種,must RunAfter和dependsOn。比如:
task task1 <<{ printfln=""'task1'=""}=""task=""task2=""<<{=""'task2'=""task2.mustRunAfter=""task1<=""code=""/>
task task1 <<{ printfln=""'task1'=""}=""task=""task2=""<<{=""'task2'=""task2.dependsOn=""task1<=""code=""/>
他們的區別是,運行的的時候前者必須要都按順序加入gradlew task2 task1執行才可以順利執行,否則單獨執行每個任務,后者只需要執行gradlew task2即可同時執行兩個任務。
Practice
我們可以通過兩個例子來實踐task。
keystore 保護
這里直接將 store 的密碼明文寫在這里對于產品的安全性來說不太好,特別是如果該源碼開源,別人就可以用你的 id 去發布app。對于這種情況,我們需要構建一個動態加載任務,在編譯release 源碼的時候從本地文件(未加入git)獲取keystore 信息,如下:
你還可以設置一個保險措施,萬一我們的沒有找到對應的文件需要用戶從控制臺輸入密碼
最后設置最終值
然后設置release 任務依賴于我們剛剛設置的任務
通過 hook Android 編譯插件 重命名 apk
最后編譯出來的apk 名字類似app-debug-1.0.apk。