這段時間來學習了gradle,也體會到了gradle從初步理解到基本熟悉,再到深入原始碼這樣一個過程中的一些曲折。於是就萌發了寫這樣一篇逐步深入原理的文章的想法。
這篇文章主要是gradle的基礎知識篇。看完這篇文章,你可以:
- 清楚gradle的定義和解決的痛點
- 基本理解Android gradle的運作機制
- 基本理解gradle的大部分語法
- 學會基本的groovy開發
如果你想關注gradle更深入的一些知識,請繼續關注後續gradle文章。
what is gradle?
先來看一段維基百科上對於gradle的解釋。
Gradle是一個基於Apache Ant和Apache Maven概念的專案自動化構建工具。它使用一種基於Groovy的特定領域語言來宣告專案設定,而不是傳統的XML。當前其支援的語言限於Java、Groovy和Scala,計劃未來將支援更多的語言。
可能剛接觸gradle的同學都不是很瞭解gradle的這個定義。可能就只會跟著網上的教程copy一點配置,但是不理解這些配置背後的原理。那麼怎麼來理解這句話呢,我們可以把握到三個要點:首先,它是一種構建工具
,其次,gradle是基於maven概念
的,最後,使用groovy
這種語言來宣告。要理解這幾句話,我們先考慮幾個場景。
1.渠道管理
:國內手機市場有大大小小數十個,大的手機廠商也有五六個,每個廠商可能又有不同的定製rom。如果我們要為不同市場和廠商進行適配,那就需要寫這樣的程式碼
if(isHuawei) {
// dosomething
} else if(isOppo) {
// dosomething
}
複製程式碼
這樣的話,繁瑣不說,對單個手機而言大量的無用程式碼被編譯進apk中,包體積和執行速度都會受影響。為了解決這個問題,gradle引進了productFlavor和buildType的能力,能根據情況來進行打包。所以說他是一個自動化構建工具
。可以看官方文件
2.依賴管理
:我們通常會在專案中引入各種三方庫進行程式碼複用。比如,直接手動把jar或者aar copy到專案中,然後新增依賴。這種方法缺陷很明顯,首先配置和刪除流程很繁瑣,其次,同一個jar可能會被多個專案所引用,導致不知不覺就copy了多個jar。最後,版本管理艱難。為了解決這個問題,gradle是基於maven倉庫,配置和刪除的時候僅需要對倉庫的座標進行操作,所有的庫都會被gradle統一管理,大多數情況下每個庫只會有一個版本存在於專案中,並且每個庫只會有一個副本存在於專案中。
所以gradle其實不是什麼神祕的東西,只是基於某種語言(groovy, java, kotlin)的一種構建工具而已。只要我們大概掌握了基本的用法和他的內部原理,日常工作中就會知道自己網上搜到的命令是什麼意思啦。skr~
小試牛刀-android中的gradle
我們們先看看日常工作中經常用到的幾個gradle檔案。可以看到主要有有三個檔案: 1.build.gradle 根檔案下放的通常放的是針對整個工程的通用配置,每個module下面的build.gradle檔案是針對每個module自身的配置。
buildscript {
ext.kotlin_version = '1.2.71'
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
allprojects {
repositories {
google()
jcenter()
}
}
複製程式碼
這是一個預設的配置,我們可以看到有buildscript,allprojects,repositories,dependencies幾個配置項,這些配置項是幹嘛的呢,很多的同學在剛學gradle的時候都是一臉懵逼的。這些其實是gradle的一種特定的語法,我們稱之為DSL(domain-specific language)。可以參考官網。這裡可以看到allprojects代理的是每個project,可以理解成我們的每個module,也就是對我們所寫的每個module的配置。buildscript主要配置的是打包相關的東西,比如gradle版本,gradle外掛版本等,這些都是針對構建工具自己的配置。repositories,dependencies是三方庫的倉庫和座標。所以根目錄的build.gradle相當於是整體的配置。
而module下的build.gradle主要是android,dependencies等配置項。
apply plugin: 'com.android.application'
android{
...
}
dependencies{
...
}
複製程式碼
可能有些同學會感到奇怪,為啥我們在官網沒有看到android這個配置項呢?這個主要是因為它並不是gradle的DSL,某種意義上說應該算是android特有的,是通過Android的外掛'com.android.application'帶進來的配置項。我們如果把第一行刪掉,就會發現android{}這個配置項找不到了。
所以,我們可以發現,build.gradle裡面的配置項,要麼是gradle自帶的,要麼是各種外掛定義的。有不認識的配置項,就去官網查詢一下就好了,授人以魚不如授人以漁嘛。我們後面也會講解到引進外掛的方式和怎麼定義外掛和配置項。
2.settings.gradle 這個檔案主要是決定每個module是否參與構建。我們可以這樣去理解,settings.gradle相當於是每個module的開關,關上了這個module就不能使用了,別的依賴到它的module也都會出問題。
3.gradle.properties 這裡主要是增加和修改一些可以在構建過程中直接使用的引數。不只是可以新增自定義引數,還可以修改系統的引數哦~
總結一下,就是說根目錄下有一個build.gradle,處理整個工程的配置項,根目錄下的settings.gradle配置整個工程中參與構建的module,每個module自己有一個build.gradle,處理自己模組的配置。這就是android構建的一個大概情況。當然,看了這一部分肯定還是不懂怎麼去寫的,接下來我們走進程式碼層面。
groovy-學gradle的金鑰
gradle可以使用groovy,kotlin,java等語言進行書寫,但是groovy相對來說是目前比較流行的gradle配置方式,下面我們講解一點groovy基礎。不講太多,夠用就行。
1.字串
groovy的字串分為兩種java.lang.String和groovy.lang.GString。其中單引號和三引號是String型別的。雙引號是GString型別的。支援佔位插值操作。和kotlin一樣,groovy的插值操作也是用${}
或者$
來標示,${}
用於一般替代字串或者表示式,$
主要用於A.B的形式中。
def number = 1
def eagerGString = "value == ${number}"
def lazyGString = "value == ${ -> number }"
println eagerGString
println lazyGString
number = 2
println eagerGString
println lazyGString
複製程式碼
2.字元Character
Groovy沒有明確的Character。但是可以強行宣告。
char c1 = 'A'
assert c1 instanceof Character
def c2 = 'B' as char
assert c2 instanceof Character
def c3 = (char)'C'
assert c3 instanceof Character
複製程式碼
4.List
Groovy的列表和python的很像。支援動態擴充套件,支援放置多種資料。使用方法支援def和直接定義。還可以像python那樣索引
//List中儲存任意型別
def heterogeneous = [1, "a", true]
//判斷List預設型別
def arrayList = [1, 2, 3]
assert arrayList instanceof java.util.ArrayList
//使用as強轉型別
def linkedList = [2, 3, 4] as LinkedList
assert linkedList instanceof java.util.LinkedList
//定義指定型別List
LinkedList otherLinked = [3, 4, 5]
assert otherLinked instanceof java.util.LinkedList
// 像python一樣索引
assert letters[1] == 'b'
//負數下標則從右向左index
assert letters[-1] == 'd'
assert letters[-2] == 'c'
//指定item賦值判斷
letters[2] = 'C'
assert letters[2] == 'C'
//給List追加item
letters << 'e'
assert letters[ 4] == 'e'
assert letters[-1] == 'e'
//獲取一段List子集
assert letters[1, 3] == ['b', 'd']
assert letters[2..4] == ['C', 'd', 'e']
複製程式碼
5.Map
//定義一個Map
def colors = [red: '#FF0000', green: '#00FF00', blue: '#0000FF']
//獲取一些指定key的value進行判斷操作
assert colors['red'] == '#FF0000'
assert colors.green == '#00FF00'
複製程式碼
6.運算子
- **: 次方運算子。
- ?.:安全佔位符。和kotlin一樣避免空指標異常。
- .@:直接域訪問操作符。因為Groovy自動支援屬性getter方法,但有時候我們有一個自己寫的特殊getter方法,當不想呼叫這個特殊的getter方法則可以用直接域訪問操作符。這點跟kotlin的
- .&:方法指標操作符,因為閉包可以被作為一個方法的引數,如果想讓一個方法作為另一個方法的引數則可以將一個方法當成一個閉包作為另一個方法的引數。
- ?::二目運算子。與kotlin中的類似。
*.
展開運算子,一個集合使用展開運算子可以得到一個元素為原集合各個元素執行後面指定方法所得值的集合。
cars = [
new Car(make: 'Peugeot', model: '508'),
null,
new Car(make: 'Renault', model: 'Clio')]
assert cars*.make == ['Peugeot', null, 'Renault']
assert null*.make == null
複製程式碼
7.閉包 groovy裡比較重要的是閉包的概念。官方定義是“Groovy中的閉包是一個開放,匿名的程式碼塊,可以接受引數,返回值並分配給變數”。 其實閉包跟kotlin的lambda函式很像,都是先定義後執行。但是又有一些細微的區別。接下來我們細講講gradle的閉包。
閉包是可以用作方法引數的程式碼塊,Groovy的閉包更象是一個程式碼塊或者方法指標,程式碼在某處被定義然後在其後的呼叫處執行。一個閉包實際上就是一個Closure型別的例項。寫法和kotlin的lambda函式很像。
我們常見的閉包是這樣的
//最基本的閉包
{ item++ }
//使用->將引數與程式碼分離
{item -> item++ }
//使用隱含引數it
{ println it }
//使用顯示的名為引數
{ name -> println name }
// 呼叫方法
a.call()
a()
// Groovy的閉包支援最後一個引數為不定長可變長度的引數。
def multiConcat = { int n, String... args ->
args.join('')*n
}
複製程式碼
大家要注意,如果我們單純的只是寫成 a = { item++ }, 這只是定義了一個閉包,是不能執行的。必須呼叫a.call()才能執行出來。所以大家可以理解了,閉包就是一段程式碼塊而已。當我們有需要的時候,可以去執行它,這麼一想是不是和lambda函式很像?
如果你看了官網,你會發現有一些這樣的說法,
什麼叫做delegate?這裡涉及到閉包內部的三種物件。
- this 對應於定義閉包的那個類,如果在內部類中定義,指向的是內部類
- owenr 對應於定義閉包的那個類或者閉包,如果在閉包中定義,對應閉包,否則同this一致
- delegate 預設是和owner一致,或者自定義delegate指向
this和owner都比較好理解。我們可以用閉包的getxxx方法獲取
def thisObject = closure.getThisObject()
def ownerObject = closure.getOwner()
def delegate = closure.getDelegate()
複製程式碼
重頭戲還是delegate這個物件。閉包可以設定delegate物件,設定delegate的意義就是將閉包和一個具體的物件關聯起來。 我們先來看個例子,這裡以自定義android閉包為例。
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0"
}
}
複製程式碼
這個閉包對應的實體類是兩個。
# Android.groovy
class Android {
private int mCompileSdkVersion
private String mBuildToolsVersion
private ProductFlavor mProductFlavor
Android() {
this.mProductFlavor = new ProductFlavor()
}
void compileSdkVersion(int compileSdkVersion) {
this.mCompileSdkVersion = compileSdkVersion
}
void buildToolsVersion(String buildToolsVersion) {
this.mBuildToolsVersion = buildToolsVersion
}
void defaultConfig(Closure closure) {
closure.setDelegate(mProductFlavor)
closure.setResolveStrategy(Closure.DELEGATE_FIRST)
closure.call()
}
@Override
String toString() {
return "Android{" +
"mCompileSdkVersion=" + mCompileSdkVersion +
", mBuildToolsVersion='" + mBuildToolsVersion + '\'' +
", mProductFlavor=" + mProductFlavor +
'}'
}
}
# ProductFlavor.groovy
class ProductFlavor {
private int mVersionCode
private String mVersionName
private int mMinSdkVersion
private int mTargetSdkVersion
def versionCode(int versionCode) {
mVersionCode = versionCode
}
def versionName(String versionName) {
mVersionName = versionName
}
def minSdkVersion(int minSdkVersion) {
mMinSdkVersion = minSdkVersion
}
def targetSdkVersion(int targetSdkVersion) {
mTargetSdkVersion = targetSdkVersion
}
@Override
String toString() {
return "ProductFlavor{" +
"mVersionCode=" + mVersionCode +
", mVersionName='" + mVersionName + '\'' +
", mMinSdkVersion=" + mMinSdkVersion +
", mTargetSdkVersion=" + mTargetSdkVersion +
'}'
}
}
複製程式碼
然後定義的時候就寫成
//閉包定義
def android = {
compileSdkVersion 25
buildToolsVersion "25.0.2"
defaultConfig {
minSdkVersion 15
targetSdkVersion 25
versionCode 1
versionName "1.0"
}
}
//呼叫
Android bean = new Android()
android.delegate = bean
android.call()
println bean.toString()
//列印結果
Android{mCompileSdkVersion=25, mBuildToolsVersion='25.0.2', mProductFlavor=ProductFlavor{mVersionCode=1, mVersionName='1.0', mMinSdkVersion=15, mTargetSdkVersion=25}}
複製程式碼
這樣就能將閉包中宣告的值,賦給兩個物件Android和ProductFlavor來處理了。
上面官網的圖裡,說ScriptHandler被設定成buildscript的delegate。意思就是buildscript定義的引數被ScriptHandler拿來使用了。大家有興趣的可以去看看ScriptHandler的原始碼~
Project與Task-gradle構建體系
上面我們講完了基本的用法,大家可能懂gradle的配置和寫法了。但是可能還是不懂gradle的構建體系到底是怎麼樣的。這裡我們就要深入進gradle的構建體系Project和Task了。下面的東西看著就要動動腦筋了。
1.Task Task是gradle指令碼中的最小可執行單元。類圖如下:
值得注意的是因為Gradle構建指令碼預設的名字是build.gradle,當在shell中執行gradle命令時,Gradle會去當前目錄下尋找名字是build.gradle的檔案。所以只有定義在build.gradle中的Task才是有效的。
可以通過三種方式來宣告task。我們可以根據自己的專案需要去定義Task。比如自定義task接管gradle的編譯過程
task myTask2 << {
println "doLast in task2"
}
//採用 Project.task(String name) 方法來建立
project.task("myTask3").doLast {
println "doLast in task3"
}
//採用 TaskContainer.create(String name) 方法來建立
project.tasks.create("myTask4").doLast {
println "doLast in task4"
}
複製程式碼
TaskContianer 是用來管理所有的 Task 例項集合的,可以通過 Project.getTasks() 來獲取 TaskContainer 例項。 常見介面:
findByPath(path: String): Task
getByPath(path: String): Task
getByName(name: String): Task
withType(type: Class): TaskCollection
matching(condition: Closure): TaskCollection
//建立task
create(name: String): Task
create(name: String, configure: Closure): Task
create(name: String, type: Class): Task
create(options: Map<String, ?>): Task
create(options: Map<String, ?>, configure: Closure): Task
//當task被加入到TaskContainer時的監聽
whenTaskAdded(action: Closure)
複製程式碼
Gradle支援增量編譯。瞭解過編譯profile檔案的朋友都知道,裡面有大量的task都是up-to-date
。那麼這種up-to-date是什麼意思呢。Gradle的Task會把每次執行的結果快取下來,當下次執行時,會檢查一個task的輸入輸出有沒有變更。如果沒有變更就是up-to-date,跳過編譯。
2.Project 先從Project物件講起,Project是與Gradle互動的主介面。android開發中最為我們所熟悉的就是build.gradle檔案,這個檔案與Project是一對一的關係,build.gradle檔案是project物件的委託,指令碼中的配置都是對應著Project的Api。Gradle構建程式啟動的時候會根據build.gradle去例項化Project類。也就是說,構建的時候,每個build.gradle檔案會生成一個Project物件,這個物件負責當前module的構建。
Project本質上是包含多個Task的容器,所有的Task存在TaskContainer中。我們從名字可以看出
可以看到dependencies, configuration, allprojects, subprojects, beforeEvaluate, afterEvaluate這些都是我們常見的配置項,在build.gradle檔案中接收一個閉包Closure。
好了,現在我們已經聊了build.gradle了,但是大家都知道,我們專案中還有一個settings.gradle呢,這個是拿來幹嘛的呢?這就要說到Project的Lifecycle
了,也就是Gradle構建Project的步驟,看官網原文:
- Create a Settings instance for the build.
- Evaluate the settings.gradle script, if present, against the Settings object to configure it.
- Use the configured Settings object to create the hierarchy of Project instances.
- Finally, evaluate each Project by executing its build.gradle file, if present, against the project. The projects are evaluated in breadth-wise order(寬度搜尋), such that a project is evaluated before its child projects. This order can be overridden by calling
Project.evaluationDependsOnChildren()
or by adding an explicit evaluation dependency usingProject.evaluationDependsOn(java.lang.String)
.
也就是說,Project物件依賴Settings物件的構建。我們常在settings.gradle檔案中配置需要引入的module,就是這個原因。
3.Property 看完了build.gradle和settings.gradle,接下來我們講講gradle.properties。這個檔案存放的鍵值對形式的屬性,這些屬效能被專案中的gradle指令碼使用ext.xxx所訪問。
我們也可以使用Properties類來動態建立屬性檔案。如:
def defaultProps = new Properties()
defaultProps.setProperty("debuggable", 'true')
defaultProps.setProperty("groupId", GROUP)
複製程式碼
並且屬性可以繼承,在一個專案中定義的屬性可以自動被子專案繼承。所以在哪個子專案都可以使用project.ext.xxx訪問。不同子專案間採用通用的配置外掛來配置
apply from: rootProject.file('library.gradle')
複製程式碼
總結
通過上面的學習,大家應該已經瞭解了gradle的基本配置,寫法和比較淺顯的內部原理了。因為篇幅原因,深入的內容我們放在下一篇。敬請期待《一篇文章深入gradle》
我是Android笨鳥之旅,一個陪著你慢慢變強的公眾號。