gradle深入研究
Gradle
1.基本元素
Project
每個專案的編譯至少有一個 Project,一個 build.gradle就代表一個project,每個project裡面包含了多個task,task裡面又包含很多action,action是一個程式碼塊,裡面包含了需要被執行的程式碼。
Script
gradle的指令碼檔案,通過指令碼,我們可以定義一個Project
Task
Project中的具體執行的原子性工作,以構建一個工程為例,它可以是 編譯,執行單元測試,釋出 等。
2.Script元素
Init Script
似乎從來沒有使用過,但是在每一次構建開始之前,都會執行init script,用來設定一些全域性變數,有多個位置可以存放init script如下:
USER_HOME/.gradle/ USER_HOME/.gradle/init.d/ GRADLE_HOME/init.d/
Settings Script
用來在組織多工程的構建,存在於root工程下,settings.gradle,用於生命該工程都包含哪些project
上述script在執行時都會被編譯成一個實現了Script介面的class,同時每一個script都有一個委託物件
Build Script -> Project Init Script-> Gradle Settings Script -> Settings
Build Script
每一個build.gradle都是一個Build Scrpit,它由兩種元素組成。
statement
可以包含方法呼叫,屬性賦值,區域性變數定義等.
script blocks
block的概念稍微複雜一點,首先我們先要理解一個groovy的元素,閉包.
有了閉包的概念,那麼理解script block就沒有障礙了,直接看文件中的定義:
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.
翻譯一下就是
一個指令碼塊是一個接受一個閉包作為引數的方法,這個閉包在執行的時候配置它的委託物件。
舉個例子:chestnut:
def buildVersion = '1.2.0' def author = 'liuboyu' allprojects { repositories { jcenter() } setVersion(buildVersion) println "this project_${name}_${getVersion()} is created by ${author}" }
首先我們定義了兩個變數分別是buildVersion和author,在執行時這個兩個變數會成為Script Class的屬性。然後,我們使用了一個script block,根據定義,這個block對應著一個同名方法allprojects,可是我們並沒有在指令碼中定義這樣一個方法,那它如何執行呢?回想一下我們剛剛看到的build script的委託物件,沒錯,這個方法被委託給了Project物件執行,檢視文件,我們確實在Project中找到了這個同名方法.
接下來,我們在塊中寫了兩行程式碼,這就是這個閉包需要執行的程式碼,首先列印一行文字,其次setVersion()。同樣的,我們沒有定義setVersion這個方法,這就涉及到閉包的一些概念,我們換一種寫法
def buildVersion = '1.2.0' def author = 'liuboyu' allprojects { repositories { jcenter() } delegate.setVersion(buildVersion) println "this project_${delegate.name}_${delegate.getVersion()} is created by ${author}" }
setVersion 這個方法實際上是由閉包的委託物件執行的,那委託物件是什麼呢?我們查閱一下allprojects這個方法的Api,如[api文件]( ofollow,noindex">https://docs.gradle.org/current/dsl/org.gradle.api.Project.html#org.gradle.api.Project:allprojects(groovy.lang.Closure)
[圖片上傳失敗...(image-76ac25-1537328769306)]
這個閉包的委託物件是當前的project和它的子project,也就是對於一個包含子工程的工程,這個閉包會執行多次,我們實驗一下
this project_GradleDeepTest_1.2.0 is created by liuboyu this project_app_1.2.0 is created by liuboyu this project_testlibrary_1.2.0 is created by liuboyu
閉包中的 Owner,delegate,this
閉包內部通常會定義一下3種類型:
- this corresponds to the enclosing class where the closure is defined
- this 對應於閉包定義處的封閉類
- owner corresponds to the enclosing object where the closure is defined, which may be either a class or a closure
- owner 對應於閉包定義處的封閉物件(可能是一個類或者閉包)
- delegate corresponds to a third party object where methods calls or properties are resolved whenever the receiver of the message is not defined
- delegate 對應於方法呼叫或屬性處的第三方物件,無論訊息接收者是否定義。
this
在閉包中,呼叫getThisObject將會返回閉包定義處所處的類。等價於使用顯示的this:
class Enclosing { void run() { // 定義在Enclosing類中的閉包,並且返回getThisObject def whatIsThisObject = { getThisObject() } // 呼叫閉包將會返回一個 閉包定義處的類的Enclosing的例項 assert whatIsThisObject() == this // 可以使用簡潔的this符號 def whatIsThis = { this } // 返回同一個物件 assert whatIsThis() == this println("Enclosing success " + this) } }
class EnclosedInInnerClass { class Inner { // 內部類中的閉包 Closure cl = { this } } void run() { def inner = new Inner() // 在內部類中的this將會返回內部類,而不是頂層的那個類。 assert inner.cl() == inner println("EnclosedInInnerClass success") } }
class NestedClosures { void run() { def nestedClosures = { // 閉包內定義閉包 def cl = { this } cl() } // this對應於最近的外部類,而不是封閉的閉包! assert nestedClosures() == this } }
class Person { String name int age String toString() { "$name is $age years old" } String dump() { def cl = { String msg = this.toString()//在閉包中使用this呼叫toString方法,將會呼叫閉包所在封閉類物件的toString方法,也就是Person的例項 println msg } cl() } } def p = new Person(name:'Janice', age:74) assert p.dump() == 'Janice is 74 years old'
Owner
閉包中的owner和閉包中的this的定義非常的像,只不過有一點微妙的不同:它將返回它最直接的封閉的物件,可以是一個閉包也可以是一個類的:
class Enclosing2 { void run() { // 定義在Enclosing類中的閉包,getOwner def whatIsThisObject = { getOwner() } // 呼叫閉包將會返回一個 閉包定義處的類的Enclosing的例項 assert whatIsThisObject() == this // 使用簡潔的owner符號 def whatIsThis = { owner } // 返回同一個物件 assert whatIsThis() == this println("Enclosing2 success " + this) } }
class EnclosedInInnerClass2 { class Inner { // 內部類中的閉包 Closure cl = { owner } } void run() { def inner = new Inner() // 在內部類中的owner將會返回內部類,而不是頂層的那個類。 assert inner.cl() == inner println("EnclosedInInnerClass success") } }
class NestedClosures2 { void run() { def nestedClosures = { // 閉包內定義閉包 def cl = { owner } cl() } // owner對應的是封閉的閉包,這是不同於this的地方 assert nestedClosures() == nestedClosures } }
Delegate
對於delegate來講,它的含義大多數情況下是跟owner的含義一樣,除非它被顯示的修改(通過Closure.setDelegate()方法進行修改)。
class Enclosing3 { void run() { // 獲得閉包的delegate可以通過呼叫getDelegate方法 def cl = { getDelegate() } // 使用delegate屬性 def cl2 = { delegate } // 二者返回同樣的物件 assert cl() == cl2() // 是封閉的類或這閉包 assert cl() == this // 特別是在閉包的內部的閉包 def closure = { // 閉包內定義閉包 def cl3 = { delegate } cl3() } // delegate對應於owner返回同樣的物件或者閉包 assert closure() == closure } }
def scriptClosure={ println "scriptClosure this:"+this println "scriptClosure owner:"+owner println "scriptClosure delegate:"+delegate } println "before setDelegate()" scriptClosure.call() scriptClosure.setDelegate ("abc") println "after setDelegate()" scriptClosure.call()
結果:
before setDelegate() scriptClosure this:class Client2 scriptClosure owner:class Client2 scriptClosure delegate:class Client2 after setDelegate() scriptClosure this:class Client2 scriptClosure owner:class Client2 scriptClosure delegate:abc
閉包的delegate可以被更改為任意的物件。先定義兩個相互之間沒有繼承關係的類,二者都定義了一個名為name的屬性:
class Person { String name def upperCasedName = { delegate.name.toUpperCase() } } def p1 = new Person(name:'Janice', age:74) def p2 = new Person(name:'liuboYu', age:18)
然後,定義一個閉包通過delegate獲取一下name屬性:
p1.upperCasedName.delegate = p2 println(p1.upperCasedName())
然後,通過改變閉包的delegate,你可以看到目標物件發生了改變:
JANICE LIUBOYU
委託機制
無論何時,在閉包中,訪問一個屬性,不需要指定接收物件,這時使用的是delegation strategy:
class Person { String name } def person = new Person(name:'Igor') def cl = { name.toUpperCase() } //name不是閉包括號內的一個變數的索引 cl.delegate = person //改變閉包的delegate為Person的例項 assert cl() == 'IGOR'//呼叫成功
之所以可以這樣呼叫的原因是name屬性將會自然而然的被delegate的對象徵用。這樣很好的解決了閉包內部屬性或者方法的呼叫。不需要顯示的設定(delegate.)作為接收者:呼叫成功是因為預設的閉包的delegation strategy使然。閉包提供了多種策略方案你可以選擇:
- Closure.OWNER_FIRST 是預設的策略。如果一個方法存在於owner,然後他將會被owner呼叫。如果不是,然後delegate將會被使用
- Closure.Delegate_FIRST 使用這樣的邏輯:delegate首先使用,其次是owner
- Closure.OWNER_ONLY 只會使用owner:delegate會被忽略
- Closure.DELEGATE_ONLY 只用delegate:忽略owner
- Closure.TO_SELF can be used by developers who need advanced meta-programming techniques and wish to implement a custom resolution strategy: the resolution will not be made on the owner or the delegate but only on the closure class itself. It makes only sense to use this if you implement your own subclass of Closure.
使用下面的程式碼來描繪一下”owner first”
class Person { String name def pretty = { "My name is $name" }//定義一個執行name的閉包成員 String toString() { pretty() } } class Thing { String name //類和Person和Thing都定義了一個name屬性 } def p = new Person(name: 'Sarah') def t = new Thing(name: 'Teapot') assert p.toString() == 'My name is Sarah'//使用預設的機制,name屬性首先被owner呼叫 p.pretty.delegate = t//設定delegate為Thing的例項物件t assert p.toString() == 'My name is Sarah'//結果沒有改變:name被閉包的owner呼叫
然而,改變closure的解決方案的策略改變結果是可以的:
p.pretty.resolveStrategy = Closure.DELEGATE_FIRST assert p.toString() == 'My name is Teapot'
通過改變resolveStrategy,我們可以改變Groovy”顯式this”的指向:在這種情況下,name將會首先在delegate中找到,如果沒有發現則是在owner中尋找。name被定義在delegate中,Thing的例項將會被使用。
“delegate first”和”delegate only”或者”owner first”和”owner only”之間的區別可以被下面的這個其中一個delegate沒有某個屬性/方法的例子來描述:
class Person { String name int age def fetchAge = { age } } class Thing { String name } def p = new Person(name:'Jessica', age:42) def t = new Things(name:'Printer') def cl = p.fetchAge cl.delegate = p assert cl() == 42 cl.delegate = t assert cl() == 42 cl.resolveStrategy = Closure.DELEGATE_ONLY cl.delegate = p assert cl() == 42 cl.delegate = t try { cl() } catch (MissingPropertyException ex) { println(" \"age\" is not defined on the delegate") }
在這個例子中,我們定義了兩個都有name屬性的類,但只有Person具有age屬性。Person類同時聲明瞭一個指向age的閉包。我們改變預設的方案策略,從”owner first”到”delegate only”。由於閉包的owner是Person類,如果delegate是Person的例項,將會成功呼叫這個閉包,但是如果我們呼叫它,且它的delegate是Thing的例項,將會呼叫失敗,並丟擲groovy.lang.MissingPropertyException。儘管這個閉包定義在Person類中,但owner沒有被使用。

image
subprojects、dependencies、repositories 都是 script blocks,後面都需要跟一個花括號,通過查閱文件可以發現,其實就是個閉包。
我們通過原始碼可以檢視
/** * <p>Configures the build script classpath for this project. * * <p>The given closure is executed against this project's {@link ScriptHandler}. The {@link ScriptHandler} is * passed to the closure as the closure's delegate. * * @param configureClosure the closure to use to configure the build script classpath. */ void buildscript(@DelegatesTo(value = ScriptHandler.class, strategy = Closure.DELEGATE_FIRST) @ClosureParams(value = SimpleType.class, options = {"org.gradle.api.initialization.dsl.ScriptHandler"}) Closure configureClosure);
它的 closure 是在一個型別為 ScriptHandler 的物件上執行的。主意用來所依賴的 classpath 等資訊。通過檢視 ScriptHandler API 可知,在 buildscript SB 中,你可以呼叫 ScriptHandler 提供的 repositories(Closure )、dependencies(Closure)函式。這也是為什麼 repositories 和 dependencies 兩個 script blocks 為什麼要放在 buildscript 的花括號中的原因。
repositories 表示程式碼倉庫的下載來源,預設的來源是jcenter。
Gradle支援的程式碼倉庫有幾種型別:
- Maven中央倉庫,不支援https訪問,宣告方法為mavenCentral()
- JCenter中央倉庫,實際上也是用Maven搭建,通過CDN分發,並且支援https訪問,也就是我們上面預設的宣告方法:jcenter
- Maven本地倉庫,可以通過本地配置檔案配置,通過USER_HOME/.m2/下的settings.xml配置檔案修改預設路徑位置,宣告方法為mavenLocal()
- 常規的第三方maven庫,需要設定訪問url,宣告方法為maven,這個一般是有自己的maven私服
- Ivy倉庫,可以是本地倉庫,也可以是遠端倉庫
- 直接使用本地資料夾作為倉庫
dependencies 表明專案依賴對應版本的Gradle構建工具,但更加具體的版本資訊卻是在gradle-wrapper.properties這個檔案中,具體如:
#Tue Oct 31 15:31:02 CST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
allprojects 指定所有參與構建的專案使用的倉庫的來源。
這裡我們有一個疑問:buildscript和allprojects都指定使用的倉庫的來源,它們的真正區別在哪裡呢?
- buildScript塊的repositories主要是為了Gradle指令碼自身的執行,獲取指令碼依賴外掛。
- 根級別的repositories主要是為了當前專案提供所需依賴包,但在Android中,這個跟allprojects的repositories的作用是一樣的。同樣dependencies也可以是根級別的。
- allprojects塊的repositories用於多專案構建,為所有專案提供共同所需依賴包。而子專案可以配置自己的repositories以獲取自己獨需的依賴包。
實際上,allprojects是用於多專案構建,在Android中,使用多專案構建,其實就是多Module構建。
我們看settings.gradle這個檔案:
include ':app', ':testlibrary'
通過include將app這個module新增進來。從根目錄開始,一直到include進來的所有module,都會執行allprojects的內容。
我們也可以通過subprojects來指定module和根目錄不同的行為。
例如:
subprojects { println " ---${delegate.name}---subprojects------ " }
上面的例子,只有子project會執行,root project並不會執行
或者我們也可以單獨指定某個project自己的行為。
我們是否可以指定某個module執行它特有的行為呢?
例如:
project(':testlibrary'){ println " ---testlibrary---subprojects------ " } project(':app'){ println " ---app---subprojects------ " }
Android自己也定義了好多ScriptBlock,請參考 DSL參考文件

image
下圖為 buildToolsVersion 和 compileSdkVersion 的說明:

image
接下來看一下
defaultConfig { applicationId "test.project.com.gradledeeptest" minSdkVersion 15 targetSdkVersion 22 versionCode 1 versionName "1.0" }
[圖片上傳失敗...(image-1b57f5-1537328769307)]
3.Gradle 組成
Gradle 主要有三種物件,這三種物件和三種不同的指令碼檔案對應,在 gradle 執行的時候,會將指令碼轉換成對應的對端:
- Gradle 物件:當我們執行 gradle xxx 或者什麼的時候,gradle 會從預設的配置指令碼中構造出一個 Gradle 物件。在整個執行過程中,只有這麼一個物件。Gradle 物件的資料型別就是 Gradle。我們一般很少去定製這個預設的配置指令碼。
- Project 物件:每一個 build.gradle 會轉換成一個 Project 物件。
- Settings 物件:顯然,每一個 settings.gradle 都會轉換成一個 Settings 物件。
4.Gradle 物件
[圖片上傳失敗...(image-2f9871-1537328769307)]
我們寫個例子,驗證只有全域性只有一個gradle,build.gradle 中和 settings.gradle 中分別加了如下輸出:
println "setting In posdevice, gradle id is " + gradle.hashCode() println "setting version: " + gradle.gradleVersion
得到結果如下所示:
setting In posdevice, gradle id is 1466991549 setting version: 4.1 library In posdevice, gradle id is 1466991549 library version: 4.1 app In posdevice, gradle id is 1466991549 app version: 4.1
- 在 settings.gradle 和 posdevice build.gradle 中,我們得到的 gradle 例項物件的 hashCode 是一樣的(都是 791279786)
- gradleVersion輸出當前 gradle 的版本
5.Project 物件
每一個 build.gradle 檔案都會轉換成一個 Project 物件。在 Gradle 術語中,Project 物件對應的是 Build Script。
Project 包含若干 Tasks。另外,由於 Project 對應具體的工程,所以需要為 Project 載入所需要的外掛,比如為 Android 工程載入 android 外掛。一個 Project 包含多少 Task 往往是外掛決定的。
- 建立一個Settings物件,
- 根據settings.gradle檔案配置它
- 根據Settings物件中定義的工程的父子關係建立Project物件
- 執行每一個工程的build.gradle檔案配置上一步中建立的Project對
載入外掛

image
apply plugin: 'com.android.library' <==如果是編譯 Library,則載入此外掛 apply plugin: 'com.android.application' <==如果是編譯 Android APP,則載入此外掛
除了載入二進位制的外掛(上面的外掛其實都是下載了對應的 jar 包,這也是通常意義上我們所理解的外掛),還可以載入一個 gradle 檔案
apply from: "utils.gradle"
6.全域性變數 ext
我們前面講解了gradle的生命週期,在配置的過程中,整個專案會生成一個gradle 物件,每個build.gradle的文件都會生成一個project物件。這兩個物件都有一個ext,這個ext的屬性就類似於我們的錢包一樣,獨立屬於gradle與project物件。我們可以往這個ext物件裡面放置屬性。
6.1 gradle 的ext物件
我們可以使用這樣的方法儲存一個變數,這個變數屬於gradle,整個工程都能使用
//第一次定義或者設定它的時候需要 ext 字首 gradle.ext.api = properties.getProperty('sdk.api') println gradle.api//再次存取 api 的時候,就不需要 ext 字首了
或者直接在 gradle.properties 檔案中新增屬性,作用域也是全域性的
讀取方式如下
task A{ doLast{ println(gradle.api) } }
6.2 project 的ext物件
儲存值
ext{ myName ='sohu' age = 18 }
獲取值,可以直接獲取
println(myName)
上面這個程式碼 println(myName) 就等於println (project.ext.myName)
我們一般在ext記憶體儲一些通用的變數,除此以外,我們也使用這個ext來做一些很酷的功能,比如說我們的gradle檔案很大了,我們可以好像程式碼一下,進行抽取。
project 的ext物件 的作用域是當前的project
7.生命週期

1212336-5ab3ae4bc237b401.png
初始化階段:
主要是解析 setting.gradle 檔案,gradle支援單工程和多工程構建,在初始化的過程中,gradle決定了這次構建包含哪些工程,並且為每一個工程建立一個Project物件。並且,所有在Settings script中包含的工程的build script都會執行,因為gradle需要為每一個Project物件配置完整的資訊。
讀取配置階段:
主要是解析所有的 projects 下的 build.gradle 檔案,在配置的過程中,本次構建包含的所有工程的build script 都會執行一次,同時每個工程的Project物件都會被配置,執行時需要的資訊在這個過程中被配置到Projec物件中。最重要的是,在build script中定義的task將在這個過程建立,並被初始化。需要注意的是,在一般情況下,只要在初始化階段建立的Project物件都會被配置,即使這個工程沒有參與本次構建。
執行階段:
按照 2 中建立的有向無迴圈圖來執行每一個 task ,整個編譯過程中,這一步基本會佔去 9 成以上的時間,尤其是對於 Android 專案來講,將 java 轉為 class.
8.專案應用
a. Gradle生命週期回撥
gradle提供了對project狀態配置監聽的介面回撥,以方便我們來配置一些Project的配置屬性,監聽主要分為兩大類,一種是通過project進行回撥,一種是通過gradle進行回撥。
作用域:
- project是隻針對當前project實現進行的監聽
- gradle監聽是針對於所有的project而言的
下面我們通過一個例子來看看gradle的構建生命週期究竟是怎麼樣的。
專案結構
root Project:GradleDeepTest -- app build.gradle -- testlibrary build.gradle build.gradle settings.gradle
root/settings.gradle
println "#### setting srcipt execute " include ':app', ':testlibrary'
root/build.gradle
println "#### root build.gradle execute " def buildVersion = '1.2.0' def author = 'liuboyu' buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:3.0.0' } } allprojects { repositories { jcenter() } //it.setVersion(buildVersion) //println "this project_${delegate.name}_${delegate.getVersion()} is created by ${author}" } gradle.addProjectEvaluationListener(new ProjectEvaluationListener() { @Override void beforeEvaluate(Project project) { println "ROOT gradle_Project lifecycle : beforeEvaluate ${project.name} evaluate " } @Override void afterEvaluate(Project project, ProjectState state) { println "ROOT gradle_Project lifecycle : afterEvaluate ${project.name} evaluate " } }) gradle.addBuildListener(new BuildListener() { @Override void buildStarted(Gradle gradle) { println "==ROOT== gradle Build lifecycle : buildStarted ${project.name} evaluate " } @Override void settingsEvaluated(Settings settings) { println "==ROOT== gradle Build lifecycle : settingsEvaluated ${project.name} evaluate " } @Override void projectsLoaded(Gradle gradle) { println "==ROOT== gradle Build lifecycle : projectsLoaded ${project.name} evaluate " } @Override void projectsEvaluated(Gradle gradle) { println "==ROOT== gradle Build lifecycle : projectsEvaluated ${project.name} evaluate " } @Override void buildFinished(BuildResult result) { println "==ROOT== gradle Build lifecycle : buildFinished ${project.name} evaluate " } })
root/app/build.gradle
println "#### app build.gradle execute " apply plugin: 'com.android.application' android { compileSdkVersion 27 buildToolsVersion "26.0.2" defaultConfig { applicationId "test.project.com.gradledeeptest" minSdkVersion 15 targetSdkVersion 25 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } project.afterEvaluate { println "%%%% app lifecycle : afterEvaluate ${project.name} evaluate " } project.beforeEvaluate { println "%%%% app lifecycle : beforeEvaluate ${project.name} evaluate " } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:22.2.0' compile 'com.android.support:design:22.2.0' }
root/testlibrary/build.gradle
println "#### library build.gradle execute " apply plugin: 'com.android.library' android { compileSdkVersion 27 buildToolsVersion "26.0.2" defaultConfig { minSdkVersion 15 targetSdkVersion 25 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } project.afterEvaluate { println "@@@@ testlibrary lifecycle : afterEvaluate ${project.name} evaluate " } project.beforeEvaluate { println "@@@@ testlibrary lifecycle : beforeEvaluate ${project.name} evaluate " } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:22.2.0' }
執行結果
#### setting srcipt execute// settings.gradle 載入 #### root build.gradle execute// root build.gradle 載入 ROOT gradle_Project lifecycle : afterEvaluate GradleDeepTest evaluate// root project 配置完成 ROOT gradle_Project lifecycle : beforeEvaluate testlibrary evaluate// testlibrary project 開始配置 #### library build.gradle execute// library build.gradle 載入 ROOT gradle_Project lifecycle : afterEvaluate testlibrary evaluate// testlibrary project 配置完成 @@@@ testlibrary lifecycle : afterEvaluate testlibrary evaluate// testlibrary project 配置完成 ROOT gradle_Project lifecycle : beforeEvaluate app evaluate// app project 開始配置 #### app build.gradle execute // app build.gradle 載入 ROOT gradle_Project lifecycle : afterEvaluate app evaluate// app project 配置完成 %%%% app lifecycle : afterEvaluate app evaluate// app project 配置完成 ==ROOT== gradle Build lifecycle : projectsEvaluated GradleDeepTest evaluate// root project 配置完成 ... BUILD SUCCESSFUL in 2s ==ROOT== gradle Build lifecycle : buildFinished GradleDeepTest evaluate // root project build完成
ps:小弟不才,有哪裡說的不對的,歡迎指正~