AS之gradle學習二(指南)
前言
為什麼需要學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 中非常重要的兩個物件。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
指令碼,我們需要手動建立。在命令列輸入以下命令即可
gradle wrapper --gradle-version 2.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
apply plugin: '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
中。示例如下:
我們可以通過這份報告看出哪個專案耗費的時間最多,哪個環節耗費的時間最多。
文/David賀(簡書作者)
原文連結:http://www.jianshu.com/p/9df3c3b6067a
著作權歸作者所有,轉載請聯絡作者獲得授權,並標註“簡書作者”。