在Android開發中,很多時候我們不需要修改 *.gradle 檔案太多,我們新增依賴、修改target compile、最低支援API level,或者修改簽名配置和build型別。其它更復雜一些邏輯,我們最後可能就是從Stack Overflow中copy了一些自己也不太懂的程式碼。本文中我們將一步一步介紹Android工程中用到的gradle檔案及其背後的原理。
1. Groovy
1.1 語法
Gradle檔案其實是用Groovy指令碼寫的,我們都會寫java,所以入門Groovy非常簡單。首先我們需要了解一下幾點:
1. 呼叫至少包含一個引數的方法時不需要使用括號:
def printAge(String name, int age) {
print("$name is $age years old")
}
def printEmptyLine() {
println()
}
def callClosure(Closure closure) {
closure()
}
printAge "John", 24 // Will print "John is 24 years old"
printEmptyLine() // Will, well, print empty line
callClosure { println("From closure") } // Will print "From closure"複製程式碼
2. 如果方法的最後一個引數是閉包(或者說是lambda表示式),可以寫在括號外(注:這個特性很重要,gradle檔案中的很多配置其實都是引數為閉包的方法):
def callWithParam(String param, Closure<String> closure) {
closure(param)
}
callWithParam("param", { println it }) // Will print "param"
callWithParam("param") { println it } // Will print "param"
callWithParam "param", { println it } // Will print "param"複製程式碼
3. 對於Groovy方法中命名過的引數,會被轉移到一個map中做為方法的第一個引數,那些沒有命名的引數則加在引數列表之後:
def printPersonInfo(Map<String, Object> person) {
println("${person.name} is ${person.age} years old")
}
def printJobInfo(Map<String, Object> job, String employeeName) {
println("$employeeName works as ${job.title} at ${job.company}")
}
printPersonInfo name: "John", age: 24
printJobInfo "John", title: "Android developer", company: "Tooploox"複製程式碼
這段程式會列印“John is 24 years old”和“John works as Android developer at Tooploox”,方法呼叫的引數可以是亂序的,map會被作為第一個引數傳入!這裡的方法呼叫也省略了括號。
1.2 閉包
閉包是一個非常重要的特性,需要解釋一下。閉包可以理解為lambada。他們是一段可以被執行的程式碼,可以有引數列表和返回值。我們可以改變一個閉包的委託:
class WriterOne {
def printText(str) {
println "Printed in One: $str"
}
}
class WriterTwo {
def printText(str) {
println "Printed in Two: $str"
}
}
def printClosure = {
printText "I come from a closure"
}
printClosure.delegate = new WriterOne()
printClosure() // will print "Printed in One: I come from a closure
printClosure.delegate = new WriterTwo()
printClosure() // will print "Printed in Two: I come from a closure複製程式碼
我們可以看到printClosure
呼叫了不同委託的printText
方法,之後會解析這個特性在gradle中的重要性。
2. Gradle
2.1 指令碼檔案
有三個主要的gradle指令碼,每個都是一個程式碼塊。
2.2 Projects
gradle 構建一般包含多個Project(在Android中每個module對應這裡的project),project中包含tasks。一般至少有一個root project,包含很多subprojects,subproject也可以巢狀project(注:Android 中對應每個library module還可以依賴其它library module)。
3. 構建基於Gradle的Android工程
Android工程中我們一般有如下的結構:
1是root project的setting檔案,被Settings
執行
2是root project的build配置
3是App project的屬性檔案,會被注入到 App的Settings
中
4是App project的build配置
3.1 建立gradle工程
我們新建一個資料夾,命名為example
,cd
進入後執行gradle projects
命令,之後就已經擁有一個gradle project了:
$ gradle projects
:projects
------------------------------------------------------------
Root project
------------------------------------------------------------
Root project 'example'
No sub-projects
To see a list of the tasks of a project, run gradle <project-path>:tasks
For example, try running gradle :tasks
BUILD SUCCESSFUL
Total time: 0.741 secs複製程式碼
3.2 配置projects層級
如果我們要建立一個預設的Android project(空的root project和一個包含Application的app project),我們就需要配置settings.gradle
, the documentation 中介紹settings.gradle
:
宣告需要例項化的配置和build的project的層級體系配置
我們通過void include(String[] projectPaths)方法來新增projects:
這裡的冒號:
用於分隔子project,可以參考這裡 here。因此我們在這裡寫:app
, 而不是直接寫app
。
在settings.gradle
中寫rootProject.name = <<name>>
也是一個比較好的實踐。如果沒有寫,那麼root project 的預設名字就是project所在資料夾的名字。
3.3 配置Android 子project
我們已經配置了root project的build.gradle
,現在來看看如何配置Android project。
從user guide可以知道我們首先要為app project配置com.android.application
外掛,我們來看看apply
方法:
void apply(Closure closure)
void apply(Map<String, ?> options)
void apply(Action<? super ObjectConfigurationAction> action)複製程式碼
儘管第三個方法很重要,我們通常使用是第二個方法,它用到我們之前提到的特性,通過map來傳遞引數。通過文件我們可以檢視可以使用哪些引數:
void apply(Map(<String, ?> options)複製程式碼
以下是可用的引數:
from: 可以引入一個指令碼apply(...),如apply from: "bintray.gradle"
從而匯入一個可用指令碼。
plugin: apply的plugin的id或者實現類
to: 委託目標
我們知道需要傳遞一個id值作為plugin
的引數,可以寫作:apply(plugin:'com.android.application')
,這裡的括號也可以省略,我們在app的build.gradle
中配置:
命令列中執行:
報錯了,找不到com.android.application
的定義,這不奇怪,我們並沒有配置,但是gradle是如何查詢Android的plugin jar包呢?在user guide可以找到答案,我們需要配置plugin的路徑。
現在我們可以在root project或者app的build.gradle
中配置路徑,但是因為buildscript
閉包是ScriptHandler
執行的,其它子project也需要使用,因此最好配置在root project的build.gradle
中:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.0-beta2'
}
}複製程式碼
如果我們在上邊的程式碼中新增括號,那麼就會發現其實都是帶有閉包引數的方法呼叫。如果我們研究下 文件,我們就可以知道是有哪些物件執行這些閉包的,總結如下:
buildscript(Closure)
是 Project
例項中呼叫的,傳遞的閉包的由ScriptHandler
執行
repositories(Closure)
是在 ScriptHandler
例項中呼叫,傳遞的閉包由 RepositoryHandler
執行
dependencies(Closure)
是在 ScriptHandler
例項中呼叫,傳遞的閉包由 DependencyHandler
執行。
也就是說 jcenter()
是由 RepositoryHandler
呼叫
classpath(String)
是由 DependencyHandler(*)
呼叫
譯者注:如果這裡看不懂的同學,可以再回頭看看groovy的語法部分,其實這裡上邊的程式碼都是方法,如buildscript是Project的方法,我們知道groovy語法中如果最後一個引數是閉包的話,可以不寫括號。
如果檢視DependencyHandler
的程式碼,我們會發現其實沒有classpath
這個方法,這是一種特殊的呼叫,我們在稍後討論。
3.4 配置Android subproject
如果我們現在執行Gradle task,依然有錯誤:
顯然,我們還沒有設定Android相關的配置,但是我們的Android plugin已經可以被正確apply了,我們增加一些配置:
android {
buildToolsVersion "25.0.1"
compileSdkVersion 25
}複製程式碼
到這裡我們知道,android方法被加入到了Project
例項中,閉包傳遞給了delegate(這裡是AppExtension),定義了buildToolsVersion
和 compileSdkVersion
方法,Android plugin使用這種方式接收所有的配置,包括default configuration,flavors等等。
想要執行gradle task,還需要兩個檔案:AndroidManifest.xml
和 local.properties
,local.properties
中配置sdk.dir
,(或者在系統環境中配置ANDROID_HOME
),指向Android SDK的位置。
3.5 擴充套件
android
方法是如何出現在Project
例項中的呢,還有我們的build.gradle是怎樣被執行的?簡單的說,Android plugin 用android這個名字註冊AppExtension
類為extension
。這個超出了本文的範圍,但是我們要知道Gradle可以為每一個註冊過的 plugin增加閉包配置。
3.6 依賴
還有一個重要的部分,dependencies還沒有討論:
dependencies {
compile 'io.reactivex.rxjava2:rxjava:2.0.4'
testCompile 'junit:junit:4.12'
annotationProcessor 'org.parceler:parceler:1.1.6'
}複製程式碼
為什麼這裡特殊呢,因為如果檢視DependencyHandler,也就是執行這個閉包的委託,它是沒有compile
,testCompile
等方法的。這個問題是有意義的,如果我們隨意增加一個freeCompile 'somelib'
,可以嗎?DependencyHandler
不會定義所有的方法,其實這裡涉及到Groory語音的另一個特性:methodMissing,這允許在執行時catch對於未定義方法的呼叫。
實際上Gradle使用了MethodMixIn中宣告的methodMissing
,類似的機制在為定義的屬性中也是一樣的。
相關的dependency操作可以在 這裡找到,它的行為如下:
如果未定義方法的呼叫方有至少一個引數,如果存在configuration()
與被呼叫方法有相同的名字,那麼就根據引數的型別和數量,呼叫具有相關引數的doAdd
方法。
每個plugin都可以增進configuration到dependencies handler中,如Android外掛增加了compile, compileClasspath, testCompile
和一些其它配置here,Android 外掛還增加了annotationProcessor
配置,根據不同build型別和產品形式還有<variant>Compile, <variant>TestCompile
等等。
由於doAdd
是私有方法,一次這裡呼叫的是公有的add
方法,我們可以重寫上邊的程式碼,但最後不要這樣做:
dependencies {
add('compile', 'io.reactivex.rxjava2:rxjava:2.0.4')
add('testCompile', 'junit:junit:4.12')
add('annotationProcessor', 'org.parceler:parceler:1.1.6')
}複製程式碼
3.7 Flavors, build types, signing configs
我們看以下程式碼:
productFlavors {
prod {
}
dev {
minSdkVersion 21
multiDexEnabled true
}
}複製程式碼
如果我們檢視原始碼,可以發現productFlavors是這樣宣告的:
void productFlavors(Action<? super
NamedDomainObjectContainer<ProductFlavorDsl>> action) {
action.execute(productFlavors)
}複製程式碼
Action<T>
是Gradle中定義的由T
執行的閉包
所有這裡我們有了NamedDomainObjectContainer
,NamedDomainObjectContainer
可以建立和配置多個ProductFlavorDsl
型別的物件,並根據ProductFlavorDsl
的名字儲存ProductFlavorDsl
。
這個容器可以使用動態方法建立指定型別的物件(這裡的ProductFlavorDsl),並和名字一起存放在容器中,所以當我們使用{}
引數呼叫prod
方法時,他被productFlavors
例項執行,執行說明如下:
NamedDomainObjectContainer
獲取到被呼叫方法的名字,生成ProductFlavorDsl
物件,配置給定的閉包,儲存方法名字到新的配置ProductFlavorDsl
的對映。
Android plugin可以從productFlavors
中獲取ProductFlavorDsl
,我們可以把它作為屬性進行訪問:productFlavors.dev
,這樣我們就可以拿到名字為dev
的ProductFlavorDsl
,這也是我們可以寫signingConfig
signingConfigs.debug
的原因。
4. 總結
對於Android開發者來說,Gradle檔案是非常常用的,並不是什麼黑魔法。但是Gradle有很多約定,而且使用Groovy語言也增加了一些複雜性,知道這兩點,Gradle並不是什麼魔法。希望瞭解通過這篇文章介紹的內容,即使是從stackoverflow中貼上程式碼,也能知道它背後的意義。
這是一篇譯文,原文作者對Android的gradle進行了比較深入的介紹,希望各位同學可以真正瞭解我們常用的gradle檔案背後的原理,而不僅僅是簡單地配置gralde。文中有些不太容易理解的地方,可以根據文中給出的連結瞭解更多內容。
原文地址medium.com/@wasyl/unde…
推薦閱讀: