理解與配置Android studio中的gradle
使用gradle構建android應用時,你總是需要這樣一個檔案:build.gradle。你應該已經看過這個檔案了,如果沒有看過的話,你現在就可以看一下,它沒有多少內容。它的簡潔性得益於它提供了很多對設定和屬性的預設值。gradle是基於groovy語言的,不過如果只是用它構建普通的工程的話,是可以不去學groovy的,如果想深入的做一下自定義的構建外掛,可以考慮學一下groovy,因為它是基於java的,所以你有java基礎的話,學習不會很難。
這篇部落格旨讓任何一個人能看懂android studio的gradle scripts,主要會從gradle的簡單語法,gradle scripts的指令碼結構,每一個指令碼(build.gradle,settings.gradle)的作用,指令碼中每一項的意義等方面說明gradle scripts.如果想了解如何詳細配置gradle,比如實現一個工程中,使用同一部分java程式碼,不同的資源res,一次生成多個不同渠道商的apk,可以看下我的這篇部落格,它對如何配置gradle有較細緻的介紹:
1.projects , tasks and action
是的,工程,任務和行為。一個專案至少要有一個工程,一個工程至少要有一個任務,一個任務由一些action組成。如果project比較抽象的話,可以這麼理解,一個build.gradle對應一個project,而action就好像java中的方法,他就是一段程式碼的集合。在工程構建的過程中,gradle會根據build.gradle中的配置資訊生成相應的project和task。
Project實質上是一系列task的集合,每一個task執行一些工作,比如編譯類檔案,解壓縮檔案,刪除檔案等等。
1.1構建過程
1.1.1初始化階段。首先會建立一個Project物件,然後執行build.gradle配置這個物件。如果一個工程中有多個module,那麼意味著會有多個Project,也就需要多個build.gradle.
1.1.2配置階段。這個階段,配置指令碼會被執行,執行的過程中,新的task會被建立並且配置給Project物件。
1.1.3執行階段。這個階段,配置階段建立的task會被執行,執行的順序取決於啟動指令碼時傳入的引數和當前目錄。
1.2 task
task標示一個邏輯上的執行單元,你可能已經用過它很多次了,不知道你有沒有意識到。當你當你重新編譯工程的時候,會用到一個叫做build 的task,當你清理工程的時候,會用到一個叫做clean 的task(後面會講到),gradle 已經為你準備了很多的task,可以使用 gradle tasks 來檢視,比如,這裡列出來一些:
assemble - Assembles all variants of all applications and secondary packages.
build - Assembles and tests this project.
buildDependents - Assembles and tests this project and all projects that depend on it.
buildNeeded - Assembles and tests this project and all projects it depends on.
clean - Deletes the build directory.
此外,你還可以自己宣告一個task,比如像這樣:
task haha {
println "haha"
}
然後使用gradle haha命令,就會打印出haha。這裡,haha這個任務被執行了,所以說task就是個執行單元。你還可以使用如下方法來定義task:
task hello << {
println "hello world"
}
這和前者是有區別的,“<<”意思是給hello這個task新增一些action,其實就是呼叫了task的doLast方法,所以,它和以下程式碼時等價的:
task hello {
doLast{
println "hello world"
}
}
關於haha 和 hello的區別,你還可以這樣加深影響:
首先,進入到你的工程目錄,執行gradle (後面沒有任何引數,另外,這個時候,build.gradle中同時有hello 和 haha 兩個tasks),結果如下:
E:\android\androidwork2.0\GradleTest>gradle
haha
Incremental java compilation is an incubating feature.
:help
Welcome to Gradle 2.13.
To run a build, run gradle <task> ...
To see a list of available tasks, run gradle tasks
To see a list of command-line options, run gradle --help
To see more detail about a task, run gradle help --task <task>
BUILD SUCCESSFUL
Total time: 21.877 secs
E:\android\androidwork2.0\GradleTest>gradle tasks
可以按到haha,被列印了,而hello沒有被列印,注意,這個時候預設執行的task 是help,也就是說並沒有執行haha 這個task,可它還是被列印了,就說明使用定義haha 這種方式定義的task在初始化階段就會被執行,而使用定義hello這種方法定義的task在執行階段才會被執行。
在android studio的頂層build.gradle中有這樣一個task:
task clean(type: Delete) {
delete rootProject.buildDir
}
可以看到這個task有一個型別type,task有很多種型別的,以下列出來一些:
這裡用到了delete型別,task的型別可以這樣理解吧:task中文就是任務,任務有很多種類,Delete就是說這是個刪除檔案的任務。
這裡就不更深入的探討task了,這些類容已經可以使我們可以理解android studio中遇到的內容了。
2.Closures
2.1 定義閉包
理解gradle需要首先理解閉包的概念,Closure就是一段程式碼塊,程式碼塊一般要用{}包起來,所以閉包的定義可以向以下的樣子:def haha = { println 'haha!' }
haha()
#output:haha!
可以看到閉包雖然可以認為是一段程式碼塊,但它可以向函式一樣呼叫,而且它還可以接受引數,比如像下面這樣:
def myClosure = {String str -> println str }
myClosure('haha!')
#output: haha!
這樣這個閉包就有引數了,多個引數只需要在->前面新增就好了。
2.2 委託
另外一個很酷的點是closure的上下文是可以改變的,通過Closure#setDelegate()。這個特性非常有用:def myClosure = {println myVar} //I'm referencing myVar from MyClass class
MyClass hello = new MyClass()
myClosure.setDelegate(hello)
myClosure()
class MyClass {
def myVar = 'Hello from MyClass!'
}
#output: Hello from MyClass!
如上所示,建立closure的時候,myVar並不存在。但是沒關係,因為當執行closure的時候,在closure的上下文中,myVar是存在的。這個例子中。因為在執行closure之前改變了它的上下文為hello,因此myVar是存在的。2.3閉包作為引數
閉包是可以作為引數的傳遞的,以下是閉包作為引數的一些情況:1.只接收一個引數,且引數是closure的方法: myMethod(myClosure)
2.如果方法只接收一個引數,括號可以省略: myMethod myClosure
3.可以使用內聯的closure: myMethod {println ‘Hello World’}
4.接收兩個引數的方法: myMethod(arg1, myClosure)
5.和4類似,單數closure是內聯的: myMethod(arg1, { println ‘Hello World’ })
6.如果最後一個引數是closure,它可以從小括號從拿出來: myMethod(arg1) { println ‘Hello World’ }
3.gradle DSL
DSL(Domain Specific Language),中文意思是特定領域的語言。gradle DSL就是gradle領域的語言。為了更好理解gradle,學習gradle DSL是有必要的。gradle的指令碼雖然非常簡短,但它有它的語法,如果不搞懂DSL,即便你知道了怎麼修改指令碼得到你想要的結果,你也不會理解為什麼要這樣修改。
3.1 你必須知道的基本概念
第一. gradle script是配置指令碼,當指令碼被執行的時候,它配置一個特定的物件。比如說,在android studio工程中,build.gradle被執行的時候,它會配置一個Project物件,settings.gradle被執行時,它配置一個Settings物件。Project,Settings這種物件就叫做委託物件,下圖展示了不同指令碼對應的不同的委託物件:
第二.每一個Gradle script實現了一個Script介面,這意味著Script介面中定義的方法和屬性都可以在指令碼中使用。
3.2構建指令碼的結構
一個構建指令碼由零個或多個statements和 script blocks組成。以下是對他們的說明,為了避免翻譯錯誤,這裡把原文貼出來。
A build script is made up of zero or more statements and script blocks. Statements can include method calls, property assignments, and local variable definitions. A script
block is a method call which takes a closure as a parameter. The closure is treated as a configuration
closure which configures some delegate object as it executes. The top level script blocks are listed below.
大概意思statments可以包括方法呼叫,屬性分配,本地變數定義;script bolck則是一個方法,它的引數可以是一個閉包。這個閉包是一個配置閉包,因為當它被執行的時候,它用來配置委託物件。以android studio的build.gradle為例:
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "com.konka.gradletest"
minSdkVersion 16
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
}
apply plugin: 'com.android.application'
以上就是一條statements,其中apply 是一個方法,後面是它的引數。這行語句之所以比較難理解是因為它使用了縮寫,寫全應該是這樣的:
project.apply([plugin: 'com.android.application'])
這樣是不是就很清楚了?project呼叫了apply方法,傳入了一個Map作為引數,這個Map的key是plugin,值是com.android.application.
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
}
它以上就是一條script block,但它卻很難被理解,之所以這麼難理解,是因為gradle語法中用了大量的簡寫,dependencies寫完整應該是這樣的:
project.dependencies({
add('compile', 'com.android.tools.build:gradle:2.0.', {
// Configuration statements
})
})
我們知道block是一個閉包,這裡首先呼叫project下的dependencies方法,這個方法的引數是一個閉包,這個閉包被傳遞給DependencyHandler,DependencyHandler有一個方法:add,這個add有三個引數,分別是'compile','...'和一個閉包。
gradle中有以下頂層build script block:
這裡再以allprojects{ }為例,說一下script block是怎麼工作的:
allprojects {
repositories {
jcenter()
}
}
allprojects{ }一般是頂層build.gradle中的一個script block,它就是一個方法,這個方法接受一個閉包作為引數。gradle工具會先建立一個Project物件,它是一個委託物件(delegate object),它建立以後,build.gradle被執行,執行的過程中,allproject{ }方法被呼叫,這個方法的引數是一個閉包,然後閉包會被執行,用來配置Project物件。
4.Understanding the Gradle files
理解了Project,task和action的概念以後,就可以就理解gradle的配置檔案了。在android studio的工程中一般會有三個配置檔案,它們各有各的功能。這三個檔案的位置應該是這樣的:
構建一個工程的時候,會有以下順序:
1.建立一個Settings物件。
2.檢查settings.gradle是否存在,不存在就什麼都不做,存在就用它來配置Settings物件。
3.使用Settings物件建立Project物件,多Module工程中,會建立一系列的Project.
4.檢查build.gradle是不是存在,存在的話就用它來配置Project物件。
4.1 settings.gradle
如果一個新的工程只包含一個android app,那麼settings.gradle應該是這樣的:
include ':app'
如果你的工程裡只有一個 app,那麼settings.gradle檔案可以不要。include ':app'中的app指明你要構建的模組名,android studio預設的模組名師app,你可以把app目錄的名字改掉,比如改成hello,那麼這個時候你就必須把settings.gradle中的app也改成hello。這會是你非常有意義的一次嘗試,因為有了這次嘗試,以後你就可以按你所願修改這個檔案了。比如就像這樣修改:
那麼這個時候你肯定已經想試試一次性構建多個app了吧?你以前如果做過,那麼你很厲害,你就不用看了,如果你沒有試過,那麼就和我一起試試吧:
第一步:在你的工程上右鍵,選擇新建mudole。
第二步:你成功了!
是的就這麼簡單,現在看看工程的樣子:
是的,這個時候,settings.gradle中多了一項,他就是我們新加的module的名字,它其實就是工程頂層目錄下的一個目錄的名字。這個名字你可以隨便改,module你也可以隨便加。
注意:settings.gradle實在初始化階段被讀入的,讀入以後會生成一個Settings物件,然後會呼叫這個物件的一些方法。你沒有必要了解這個物件,你知道它的存在對你理解專案構建的過程有所幫助。
4.2 The top-level build file
就是頂層的build.gradle指令碼。這個檔案中配置內容將會應用到所有modules中(上一步我們已經建立了兩個module了,一個hello,一個gradletest2)。所以,每個module中都有的共同的屬性,都會在頂層的build.gradle中配置,它預設有以下內容:
<pre style="font-family: 宋體; font-size: 9pt; background-color: rgb(255, 255, 255);"><pre name="code" class="java">buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.0.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
這個指令碼是由buildscript {},allprojects{} 兩個script block組成,buildsctipt是一個頂層的build script block,正如2.2中所說的那樣,是一個方法,引數是一個閉包,這個閉包裡面又有一些script block,這些script bolck也是方法,引數也是一個閉包。最終這些閉包會被執行,用來配置對應的委託物件。比如,repositories這個方法的閉包呼叫了jcenter方法,這個方法會配置gradle的遠端倉庫,配置好了以後,在工程構建過程中,如果缺少依賴,就會在遠端倉庫中查詢。頂層build.gradle中的配置會應用到所有的工程中,頂層build.gradle的委託物件是root Project,子工程目錄下的build.gradle對應它自己的Project,總之,一個build.gradle對應一個Project。
至於每個script block的意義,但從字面意思上就能猜出一些來,比如allprojects {}就是為所有的project配置閉包中的內容,這裡就是配置遠端倉庫,倉庫有很多種,想使用其他倉庫就可以在這裡修改。buildsctipt{}為所有project配置構建用的倉庫的工具,它裡面的dependecbies{}就是配置構建工具的資訊,從中可以看到構建工具是gradle,版本是2.0.0;所以,修改gradle的版本就可以在這裡改。不過單從名字得到的資訊是遠遠不夠的,為了獲取更多的資訊,你可以看看《gradle for android》這本書。
4.3子工程下的build.gradle
apply plugin: 'com.android.application'
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
defaultConfig {
applicationId "com.konka.gradletest"
minSdkVersion 16
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:23.3.0'
}
4.3.1第一行是一個statement,呼叫了apply方法,這個方法的定義如下:
void apply(Map<String, ?> options);
它的作用是檢查gradle有沒有所宣告的這個外掛,有就什麼都不做,沒有的話就會使外掛可用。
具體的每一個script block,它們大部分都是都是方法,都可以在android studio 中按住ctrl+滑鼠左鍵,點進去看它的宣告,每個方法都有註釋來解釋它的作用。
4.3.1 android block
android 是這個指令碼中最大的塊,它包含了andoird特有的外掛,這些外掛可以使用是因為之前呼叫了
apply plugin: 'com.android.application',
此外,這裡設定了編譯android用的引數,構建型別等。
4.3.2 dependencies block
dependecies也是一個接受閉包作為引數的方法,這裡設定了編譯當前的app的依賴。如果當前app依賴外部的包,可以把這個包放到libs目錄下面,然後右鍵,選擇add as library,然後就會在這裡生成一條compile ' ... '的記錄。
4.3.3還有其他的一些配置,比如:
//這個是解決lint報錯的程式碼
lintOptions {
abortOnError false
}
//<span style="font-family: Arial, Helvetica, sans-serif; font-size: 9pt;">簽名設定</span>
signingConfigs {
myConfigs {
storeFile file("簽名檔案地址")
keyAlias "..."
keyPassword "..."
storePassword "..."
}
}
//<span style="font-family: Arial, Helvetica, sans-serif; font-size: 9pt;">混淆設定</span>
buildTypes {
release {
signingConfig signingConfigs.myConfigs
runProguard true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
//<span style="font-family: Arial, Helvetica, sans-serif; font-size: 9pt;">渠道打包(不同包名)</span>
productFlavors {
aaa{
applicationId = '包名'
}
bbb{
applicationId='包名'
}
}
}
//<span style="font-family: Arial, Helvetica, sans-serif; font-size: 9pt;">so檔案的匯入</span>
task copyNativeLibs(type: Copy) {
from fileTree(dir: 'libs', include: 'armeabi/*.so') into 'build/lib'
}
總結:以上的所有內容展示了一個gradle工作的大致過程,gradle指令碼的組成方式,概括性的介紹了android studio中每個gradle配置指令碼的功能,大概的闡述了一些script block的作用。由於這篇部落格旨在理解android studio的gradle的工作方式和指令碼的做成結構,所以,如果想更詳細的理解每一個script block的作用,可以看下《gradle for android》這本書。此外,後續的文章也會有詳細的對常用script block的探討。