Gradle外掛從入門到進階

渡口一艘船發表於2019-05-05

Gradle外掛從入門到進階

1、簡介

Gradle本身的領域物件主要有Project和Task。Project為Task提供了執行上下文,所有的Plugin要麼向Project中新增用於配置的Property,要麼向Project中新增不同的Task。一個Task表示一個邏輯上較為獨立的執行過程,比如編譯Java原始碼,拷貝檔案,打包Jar檔案,甚至可以是執行一個系統命令或者呼叫Ant。另外,一個Task可以讀取和設定Project的Property以完成特定的操作。

Groovy 基礎

Android DSL 基礎

相關程式碼在 github.com/xsfelvis/Gr…

2、核心概念

Project物件

自定義外掛類是通過實現Plugin 介面,並將 org.gradle.api.Project作為模板引數,其中org.gradle.api.Project的例項物件將作為引數傳給void apply(Project project)函式,根據官網,可以看出Project是與Gradle互動的主介面,通過Project可以使用gradle的所有特性,並且 Project與build.grale是一對一的關係。簡而言之,就是通過程式碼使用Gradle,通過Project這個入口即可

我們對project的理解更多來源於專案目錄中的build.gradle檔案(因為它其實就是project物件的委託,在指令碼中的配置方法都對應著Project中的API,當構建程式啟動後Gradle基於build.gradle中的配置例項化org.gradle.api.Project類,本質上可以認為是包含多個Task的容器,所有的Task都存放在TaskContainer中,Project物件的類圖如下所示:

image.png

專案配置

在build.gradle指令碼檔案中,我們不僅可以對單獨project進行配置,也可以定義project塊的共有邏輯等,參考下面的定義。

image.png

常見的例子

// 為所有專案新增倉庫源配置
allprojects {
    repositories {
        jcenter()
        google()
    }
}
// 為所有子專案新增mavenPublish的配置塊
subprojects {
    mavenPublish {
        groupId = maven.config.groupId
        releaseRepo = maven.config.releaseRepo
        snapshotRepo = maven.config.snapshotRepo
    }
}
複製程式碼

Task

Gradle Task API

Gradle構建指令碼預設的名字是build.gradle,當在shell中執行gradle命令時,Gradle會去當前目錄下尋找名字是build.gradle的檔案。在Gradle中一個原子性的操作叫做task,簡單理解為task是Gradle指令碼中的最小可執行單元。

下面是task的類圖。

image.png

Task的Actions

一個Task是由一序列Action組成的,當執行一個Task的時候,這個Task裡的Action序列會按照順序執行

Task的幾種常見寫法

task myTask1 {
    doLast {
        println "doLast in task1"
    }
}

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"
}

project.tasks.create("myTask5") << {
    println "doLast in task5"
}




複製程式碼

目前task的動作(action)宣告主要包含兩個方法:

  • doFirst  等價操作 縮寫 leftShift <<(5.0會廢棄)
  • doLast

在 Gradle 中定義 Task 的時候,可以指定更多的引數,如下所示:

引數名 含義 預設值
name task的名字 必須指定,不能為空
type task的父類 預設值為org.gradle.api.DefaultTask
overwrite 是否替換已經存在的同名task false
group task所屬的分組名 null
description task的描述 null
dependsOn task依賴的task集合
constructorArgs 建構函式引數

Task的依賴

gradle中任務的執行順序是不確定的。通過task之間的依賴關係,gradle能夠確保所依賴的task會被當前的task先執行。使用task的dependsOn()方法,允許我們為task宣告一個或者多個task依賴。

task first{
    doLast{
        println("first")
    }
}

task second{
    doLast{
        println("second")
    }
}

task third{
    doLast{
        println("third")
    }
}

task test(dependsOn:[second,first]){
    doLast{
        println("first")
    }
}

third.dependsOn(test)
複製程式碼

執行順序

> Task :app:first 


> Task :app:second 


> Task :app:test1 


> Task :app:third 
複製程式碼

Task的型別

有copy、jar、Delete 等等 可以參考Doc文件

task copyFile(type: Copy) {
   from 'xml'
   into 'destination'
}

自定義Task

Gradle 中通過 task 關鍵字建立的 task,預設的父類都是 org.gradle.api.DefaultTask,這裡定義了一些 task 的預設行為。看看下面這個例子:

//自定義Task類,必須繼承自DefaultTask
class SayHelloTask extends DefaultTask {
    
    String msg = "default name"
    int age = 18        

    //建構函式必須用@javax.inject.Inject註解標識
    @javax.inject.Inject
    SayHelloTask(int age) {
        this.age = age
    }

    //通過@TaskAction註解來標識該Task要執行的動作
    @TaskAction
    void sayHello() {
        println "Hello $msg ! age is ${age}"
    }

}

//通過constructorArgs引數來指定建構函式的引數值
task hello1(type: SayHelloTask, constructorArgs: [30])

//通過type引數指定task的父類,可以在配置程式碼裡修改父類的屬性
task hello2(type: SayHelloTask, constructorArgs: [18]) {
        //配置程式碼裡修改 SayHelloTask 裡的欄位 msg 的值
    msg = "hjy"
}
複製程式碼


執行這兩個task

> Task :hello1
Hello default name ! age is 30

> Task :hello2
Hello hjy ! age is 18
複製程式碼

### Task的類圖 Gradle所說的Task是org.gradle.api.Task介面,預設實現是org.gradle.api.DefaultTask類,其類圖如下
![image.png](https://cdn.nlark.com/yuque/0/2019/png/215777/1556193021427-9d3cda16-11cd-4908-8f9c-aac56947ad6e.png#align=left&display=inline&height=806&name=image.png&originHeight=1081&originWidth=1000&size=214151&status=done&width=746)

TaskContainer介面解析

TaskContianer 是用來管理所有的 Task 例項集合的,可以通過 Project.getTasks() 來獲取 TaskContainer 例項。

org.gradle.api.tasks.TaskContainer介面:
//查詢task
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)


//當有task建立時
getTasks().whenTaskAdded { Task task ->
    println "The task ${task.getName()} is added to the TaskContainer"
}

//採用create(name: String)建立
getTasks().create("task1")

//採用create(options: Map<String, ?>)建立
getTasks().create([name: "task2", group: "MyGroup", description: "這是task2描述", dependsOn: ["task1"]])

//採用create(options: Map<String, ?>, configure: Closure)建立
getTasks().create("task3", {
    group "MyGroup"
    setDependsOn(["task1", "task2"])
    setDescription "這是task3描述"
})

複製程式碼

預設情況下,我們常見的task都是org.gradle.api.DefaultTask型別。但是在gradle當中有相當豐富的task型別我們可以直接使用。要更改task的型別,我們可以參考下面的示例

task createDistribution(type:Zip){
    
}
複製程式碼

更多關於task的型別,可以參考gradle的官方文件

www.jianshu.com/p/cd1a78dc8…
www.ezlippi.com/blog/2015/0…
blog.csdn.net/lzyzsd/arti…

Task的增量構建

Gradle 支援一種叫做 up-to-date 檢查的功能,也就是常說的增量構建。Gradle 的 Task 會把每次執行的結果快取下來,當下次執行時,會檢查輸出結果有沒有變更,如果沒有變更則跳過執行,這樣可以提高 Gradle 的構建速度。
通常,一個 task 會有一些輸入(inputs)和一些輸出(outputs),task 的輸入會影響其輸出結果,以官網中的一張圖為例:

image.png

圖中表示一個java編譯的task,它的輸入有2種,一是JDK版本號,一是原始檔,它的輸出結果為class檔案,只要JSK版本號與原始檔有任何變動,最終編譯出的class檔案肯定不同的。當我們執行過一次·編譯任務之後,再次執行該task,如果發現他的輸入沒有任何改動,那麼它編譯後的結果肯定也是不變的,可以直接從快取裡獲取輸出,這樣Gradle會標識該task為UP-TO-DATE,從而跳過該task的執行

TaskInputs、TaskOutputs介紹

如何實現一個增量構建呢,至少指定一個輸入,一個輸出,Task.getInputs() 物件型別為 TaskInputs,Task.getOutputs() 物件型別為 TaskOuputs,從中也可以看到inputs、outputs都支援哪些資料型別

task test1 {
    //設定inputs
    inputs.property("name", "hjy")
    inputs.property("age", 20)
    //設定outputs
    outputs.file("$buildDir/test.txt")

    doLast {
        println "exec task task1"
    }
}

task test2 {
    doLast {
        println "exec task task2"
    }
}

//第一次的執行結果
> Task :test1
exec task task1

> Task :test2
exec task task2

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed

//第二次的執行結果
> Task :test2
exec task task2

BUILD SUCCESSFUL in 0s
2 actionable tasks: 1 executed, 1 up-to-date
複製程式碼

從結果中可以看到,第2次執行時,test1 task 並沒有執行,而是被標記為 up-to-date,而 test2 task 則每次都會執行,這就是典型的增量構建。

taskInputs、taskOutputs註解

可以通過task註解來實現增量構建,這是一種更加靈活方便的方式

註解名 屬性型別 描述
@Input 任意Serializable型別 一個簡單的輸入值
@InputFile File 一個輸入檔案,不是目錄
@InputDirectory File 一個輸入目錄,不是檔案
@InputFiles Iterable File列表,包含檔案和目錄
@OutputFile File 一個輸出檔案,不是目錄
@OutputDirectory File 一個輸出目錄,不是檔案
@OutputFiles Map<String, File>或Iterable 輸出檔案列表
@OutputDirectories Map<String, File>或Iterable 輸出目錄列表
class SayHelloTask extends DefaultTask {
    
    //定義輸入
    @Input
    String username;
    @Input
    int age

    //定義輸出
    @OutputDirectory
    File destDir;

    @TaskAction
    void sayHello() {
        println "Hello $username ! age is $age"
    }

}

task test(type: SayHelloTask) {
    age = 18
    username = "hjy"
    destDir = file("$buildDir/test")
}
複製程式碼

Property

ext名稱空間

Gradle中很多模型類都提供了特別的屬性支援,比如Project.在gradle內部,這些屬性會以鍵值對的形式儲存。使用ext名稱空間,我們可以方便的新增屬性。下面的方式都是支援的:

//在project中新增一個名為groupId的屬性
project.ext.groupId="tech.easily"
// 使用ext塊新增屬性
ext{
    artifactId='EasyDependency'
    config=[
            key:'value'
    ]
}
複製程式碼

值得注意的是,只有在宣告屬性的時候我們需要使用ext名稱空間,在使用屬性的時候,ext名稱空間是可以省略的。

屬性檔案

正如我們經常在Android專案中看到的,我們可以在專案的根目錄下新建一個gradle.properties檔案,並在檔案中定義簡單的鍵值對形式的屬性。這些屬效能夠被專案中的gradle指令碼所訪問。如下所示:

# gradle.properties
# 注意檔案的註釋是以#開頭的
groupId=tech.easily
artifactId=EasyDependency
複製程式碼
複製程式碼

有的時候,我們可能需要在程式碼中動態的建立屬性檔案並讀取檔案中的屬性(比如自定義外掛的時候),我們可以使用java.util.Properties類。比如:

void createPropertyFile() {
    def localPropFile = new File(it.projectDir.absolutePath + "/local.properties")
    def defaultProps = new Properties()
    if (!localPropFile.exists()) {
        localPropFile.createNewFile()
        defaultProps.setProperty("debuggable", 'true')
        defaultProps.setProperty("groupId", GROUP)
        defaultProps.setProperty("artifactId", project.name)
        defaultProps.setProperty("versionName", VERSION_NAME)
        defaultProps.store(new FileWriter(localPropFile), "properties auto generated for resolve dependencies")
    } else {
        localPropFile.withInputStream { stream ->
            defaultProps.load(stream)
        }
    }
}
複製程式碼

關於屬性很重要的一點是屬性是可以繼承的。在一個專案中定義的屬性會自動的被其子專案繼承,不管我們是用以上哪種方式新增屬性都是適用的。

ExtensionContainer

Extension簡介

就是 Gradle 的 Extension,翻譯成中文意思就叫擴充套件。它的作用就是通過實現自定義的 Extension,可以在 Gradle 指令碼中增加類似 android 這樣名稱空間的配置,Gradle 可以識別這種配置,並讀取裡面的配置內容。

一般我們通過ExtensionContainer來建立Extension,這個類跟TaskContainer命名有點類似。TaskContainer是用來建立並管理Task的,而ExtensionContainer則是用來建立並管理Extension的,通過Project的以下API可以獲取到ExtensionContainer物件

ExtensionContainer getExtensions()
複製程式碼

簡單的Extension

/先定義一個普通的java類,包含2個屬性
class Foo {
    int age
    String username
    String toString() {
        return "name = ${username}, age = ${age}"
    }
}
//建立一個名為 foo 的Extension
getExtensions().create("foo", Foo)
//配置Extension
foo {
    age = 30
    username = "hjy"
}
task testExt.doLast {
    //能直接通過 project 獲取到自定義的 Extension
    println project.foo
}
複製程式碼

foo 就是我們自定義的 Extension 了,它裡面能配置的屬性與類 Foo 中的欄位是一致的,在 build.gradle 中可以直接通過 project.foo 來訪問。每個 Extension 實際上與某個類是相關聯的,在 build.gradle 中通過 DSL 來定義,Gradle 會識別解析並生成一個物件例項,通過該類可以獲取我們所配置的資訊。
 Project 有個擴充套件屬性是通過 ext 名稱空間配置的,可以看到 ext 與這裡是類似的,不同的是 ext 可以配置任何鍵值對的屬性值,而這裡只能識別我們定義的 Java 類裡的屬性值。

ExtensionContainer主要api及用法

<T> T create(String name, Class<T> type, Object... constructionArguments)
<T> T create(Class<T> publicType, String name, Class<? extends T> instanceType, Object... constructionArguments)
複製程式碼

先來看看後面這個 API 所有引數的含義。

  • publicType:建立的 Extension 例項暴露出來的類型別;
  • name:要建立的Extension的名字,可以是任意符合命名規則的字串,不能與已有的重複,否則會拋異常;
  • instanceType:該Extension的類型別;
  • constructionArguments:類的建構函式引數值

官方文件裡還說明了一個特性,建立的 Extension 物件都預設實現了 ExtensionAware 介面,並註明出處。

示例

//父類
class Animal {
    
    String username
    int legs

    Animal(String name) {
        username = name
    }
    
    void setLegs(int c) {
        legs = c
    }

    String toString() {
        return "This animal is $username, it has ${legs} legs."
    }
}

//子類
class Pig extends Animal {
    
    int age
    String owner

    Pig(int age, String owner) {
        super("Pig")
        this.age = age
        this.owner = owner
    }

    String toString() {
        return super.toString() + " Its age is $age, its owner is $owner."
    }

}

//建立的Extension是 暴露出來Animal 型別,建立extension名稱是name,該extension的型別是Pig,後面2個是引數
Animal aAnimal = getExtensions().create(Animal, "animal", Pig, 3, "hjy")
//建立的Extension是 Pig 型別
Pig aPig = getExtensions().create("pig", Pig, 5, "kobe")

animal {
    legs = 4    //配置屬性
}

pig {
    setLegs 2   //這個是方法呼叫,也就是 setLegs(2)
}

task testExt << {
    println aAnimal
    println aPig
    //驗證 aPig 物件是 ExtensionAware 型別的
    println "aPig is a instance of ExtensionAware : ${aPig instanceof ExtensionAware}"
}
複製程式碼

增加Extension

  • create() 方法會建立並返回一個 Extension 物件,
  • add() 方法,唯一的差別是它並不會返回一個 Extension 物件

基於前面的這個例項,我們可以換一種寫法如下:

getExtensions().add(Pig, "mypig", new Pig(5, "kobe"))
mypig {
    username = "MyPig"
    legs = 4
    age = 1
}
task testExt << {
    def aPig = project.getExtensions().getByName("mypig")
    println aPig
}
複製程式碼

查詢Extension

Object findByName(String name)
<T> T findByType(Class<T> type)
Object getByName(String name)       //找不到會拋異常
<T> T getByType(Class<T> type)  //找不到會拋異常
複製程式碼

巢狀Extension 方式一

類似下面這樣的配置應該隨處可見:

outer {
    
    outerName "outer"
    msg "this is a outer message."
    inner {
        innerName "inner"
        msg "This is a inner message."
    }
    
}
複製程式碼

可以通過下面的方式來建立

class OuterExt {
    
    String outerName
    String msg
    InnerExt innerExt = new InnerExt()

    void outerName(String name) {
        outerName = name
    }

    void msg(String msg) {
        this.msg = msg
    }
    
    //建立內部Extension,名稱為方法名 inner
    void inner(Action<InnerExt> action) {
        action.execute(inner)
    }

    //建立內部Extension,名稱為方法名 inner
    void inner(Closure c) {
        org.gradle.util.ConfigureUtil.configure(c, innerExt) 
    }

    String toString() {
        return "OuterExt[ name = ${outerName}, msg = ${msg}] " + innerExt
    }

}


class InnerExt {
    
    String innerName
    String msg

    void innerName(String name) {
        innerName = name
    }

    void msg(String msg) {
        this.msg = msg
    }

    String toString() {
        return "InnerExt[ name = ${innerName}, msg = ${msg}]"
    }

}

def outExt = getExtensions().create("outer", OuterExt)

outer {
    
    outerName "outer"
    msg "this is a outer message."

    inner {
        innerName "inner"
        msg "This is a inner message."
    }

}

task testExt doLast {
    println outExt
}
複製程式碼

關鍵在以下下面的方法

void inner(Action<InnerExt> action)
void inner(Closure c)
複製程式碼

定義在outer內部的inner,Gradle 解析時本質上會呼叫 outer.inner(……)方法,該方法的引數是一個閉包(Script Block) 所以在類OuterExt中必須定義inner方法

巢狀Extension方式二(NamedDomainObjectContainer)

使用場景

Gradle Extension 的時候,說到名為 android 的 Extension 是由 BaseExtension 這個類來實現的,裡面對 buildTypes 是這樣定義的:

private final NamedDomainObjectContainer<BuildType> buildTypes;
複製程式碼

buildTypes 就是 NamedDomainObjectContainer 型別的,先來看看 buildTypes 在 Android 中是怎麼使用的,下面這段程式碼應該都很熟悉了,它定義了 debug、relase 兩種打包模式:

android {
    buildTypes {
        release {
            // 是否開啟混淆
            minifyEnabled true
            // 開啟ZipAlign優化
            zipAlignEnabled true
            //去掉不用資源
            shrinkResources true
            // 混淆檔案位置
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            // 使用release簽名
            signingConfig signingConfigs.hmiou
        }
        debug {
            signingConfig signingConfigs.hmiou
        }
    }
}
複製程式碼

當我們新建一個專案時候,預設會有debug和release這2個配置,那麼debug、release可以修改其他名字嗎,能增加其他名字來配置嗎,比如想增加一個測試包配置test,還有就是release裡面都能配置哪些屬性呢
我來說下結果,如果不確定的,可以實際驗證一下:

  • debug、release 是可以修改成其他名字的,你可以替換成你喜歡的名字;
  • 你可以增加任意不同名字的配置,比如增加一個開發版本的打包配置 dev ;
  • 可配置的屬性可參考介面:com.android.builder.model.BuildType ;

可以看到它是非常靈活的,可以根據不同的場景定義不同的配置,每個不同的名稱空間都會生成一個 BuildType 配置。要實現這樣的功能,必須使用 NamedDomainObjectContainer 型別。

什麼是NamedDomainObjectContainer

顧名思義就是命名領域物件容器,它的主要功能有:

  • 通過DSL建立指定type的物件例項
  • 指定的type必須有一個public建構函式,且必須帶有一個String name的引數
  • 它是一個實現了SortedSet介面的容器,所以所有領域物件的name屬性都必須是唯一的,在容器內部會用name屬性來排序

named domain object container is a specialisation of NamedDomainObjectSet that adds the ability to create instances of the element type.  Note that a container is an implementation of SortedSet, which means that the container is guaranteed to only contain elements with unique names within this container. Furthermore, items are ordered by their name.

建立NamedDomainObjectContainer

NamedDomainObjectContainer 需要通過 Project.container(...) API 來建立,其定義為:

<T> NamedDomainObjectContainer<T> container(Class<T> type)
<T> NamedDomainObjectContainer<T> container(Class<T> type, NamedDomainObjectFactory<T> factory)
<T> NamedDomainObjectContainer<T> container(java.lang.Class<T> type, Closure factoryClosure
複製程式碼

來看個具體的例項:

//這是領域物件型別定義
class TestDomainObj {
    
    //必須定義一個 name 屬性,並且這個屬性值初始化以後不要修改
    String name

    String msg

    //建構函式必須有一個 name 引數
    public TestDomainObj(String name) {
        this.name = name
    }

    void msg(String msg) {
        this.msg = msg
    }

    String toString() {
        return "name = ${name}, msg = ${msg}"
    }
}

//建立一個擴充套件
class TestExtension {

    //定義一個 NamedDomainObjectContainer 屬性
    NamedDomainObjectContainer<TestDomainObj> testDomains

    public TestExtension(Project project) {
        //通過 project.container(...) 方法建立 NamedDomainObjectContainer 
        NamedDomainObjectContainer<TestDomainObj> domainObjs = project.container(TestDomainObj)
        testDomains = domainObjs
    }

    //讓其支援 Gradle DSL 語法
    void testDomain(Action<NamedDomainObjectContainer<TestDomainObj>> action) {
        action.execute(testDomains)
    }

    void test() {
        //遍歷命名領域物件容器,列印出所有的領域物件值
        testDomains.all { data ->
            println data        
        }
    }
}

//建立一個名為 test 的 Extension
def testExt = getExtensions().create("test", TestExtension, project)

test {
    testDomain {
        domain2 {
            msg "This is domain2"
        }
        domain1 {
            msg "This is domain1"
        }
        domain3 {
            msg "This is domain3"
        }
    }   
}

task myTask doLast {
    testExt.test()
}
複製程式碼

執行結果如下:

name = domain1, msg = This is domain1
name = domain2, msg = This is domain2
name = domain3, msg = This is domain3
複製程式碼

查詢和遍歷

NamedDomainObjectContainer 既然是一個容器類,與之相應的必然會有查詢容器裡的元素和遍歷容器的方法:

//遍歷
void all(Closure action)
//查詢
<T> T getByName(String name)
//查詢
<T> T findByName(String name)
複製程式碼

還是接著前面的例子:

//通過名字查詢
TestDomainObj testData = testDomains.getByName("domain2")
println "getByName: ${testData}"
//遍歷命名領域物件容器,列印出所有的領域物件值
testDomains.all { data ->
    println data        
}
複製程式碼

需要注意的是,Gradle 中有很多容器類的迭代遍歷方法有 each(Closure action)、all(Closure action),但是一般我們都會用 all(...) 來進行容器的迭代。all(...) 迭代方法的特別之處是,不管是容器內已存在的元素,還是後續任何時刻加進去的元素,都會進行遍歷。

Android的Extension

我們在gradle中會看到 android{}

image.png

defaultConfig、productFlavors、signingConfigs、buildTypes 這4個內部 Extension物件是怎麼定義的,通過檢視原始碼可以找到一個叫 BaseExtension 的類,裡面的相關程式碼如下:

private final DefaultConfig defaultConfig;
    private final NamedDomainObjectContainer<ProductFlavor> productFlavors;
    private final NamedDomainObjectContainer<BuildType> buildTypes;
    private final NamedDomainObjectContainer<SigningConfig> signingConfigs;
    public void defaultConfig(Action<DefaultConfig> action) {
        this.checkWritability();
        action.execute(this.defaultConfig);
    }
    
     public void buildTypes(Action<? super NamedDomainObjectContainer<BuildType>> action) {
        this.checkWritability();
        action.execute(this.buildTypes);
    }
    public void productFlavors(Action<? super NamedDomainObjectContainer<ProductFlavor>> action) {
        this.checkWritability();
        action.execute(this.productFlavors);
    }
    public void signingConfigs(Action<? super NamedDomainObjectContainer<SigningConfig>> action) {
        this.checkWritability();
        action.execute(this.signingConfigs);
    }
複製程式碼

在 app 的 build.gradle 裡我們通常會採用外掛 apply plugin: 'com.android.application' ,而在 library module 中則採用外掛 apply plugin: 'com.android.library',AppPlugin 就是外掛 com.android.application 的實現類,LibraryPlugin 則是外掛 com.android.library 的實現類,接著再看看 AppPlugin 裡是怎樣建立 Extension 的:

public class AppPlugin extends BasePlugin implements Plugin<Project> {
    @Inject
    public AppPlugin(Instantiator instantiator, ToolingModelBuilderRegistry registry) {
        super(instantiator, registry);
    }
    protected BaseExtension createExtension(Project project, ProjectOptions projectOptions, Instantiator instantiator, AndroidBuilder androidBuilder, SdkHandler sdkHandler, NamedDomainObjectContainer<BuildType> buildTypeContainer, NamedDomainObjectContainer<ProductFlavor> productFlavorContainer, NamedDomainObjectContainer<SigningConfig> signingConfigContainer, NamedDomainObjectContainer<BaseVariantOutput> buildOutputs, ExtraModelInfo extraModelInfo) {
        return (BaseExtension)project.getExtensions().create("android", AppExtension.class, new Object[]{project, projectOptions, instantiator, androidBuilder, sdkHandler, buildTypeContainer, productFlavorContainer, signingConfigContainer, buildOutputs, extraModelInfo});
    }
    public void apply(Project project) {
        super.apply(project);
    }
    //省略...
}
複製程式碼

在 createExtension() 方法中,可以看到建立了一個名為 android 的 Extension,該 Extension 的型別為 AppExtension,而 AppExtension 的繼承結構為 AppExtension -> TestedExtension -> BaseExtension,所以它的實現邏輯大部分都是在 BaseExtension 裡實現的。

在Android 工程中的build.gradle 檔案中,我們配置相關資訊使用 android{} 節點,從 AppPlugin 也能看出其 Extension的名稱為 android ,所以獲取方法如下:

  • project.extensions.getByName
  • project.extensions.getByType

``` def getInfo() { //或者 直接 project.android BaseExtension extension = project.extensions.getByName("android") def android = project.extensions.getByType(AppExtension) project.android
println "buildToolsVersion:${extension.buildToolsVersion}"
println "compileSdkVersion:${extension.getCompileSdkVersion()}"
println "applicationId:${extension.defaultConfig.applicationId}"
println "minSdkVersion:${extension.defaultConfig.minSdkVersion}"
println "targetSdkVersion:${extension.defaultConfig.targetSdkVersion}"
println "versionCode:${extension.defaultConfig.versionCode}"
println "versionName:${extension.defaultConfig.versionName}"
複製程式碼

}


更詳細的請參考 

- [ASL](http://google.github.io/android-gradle-dsl/current/index.html)
<a name="apKmL"></a>
# 3、構建生命週期

<a name="6743b681"></a>
### 三個階段

每次構建的**本質其實就是執行一系列的Task**,某些Task可能依賴其他Task,那些沒有依賴的Task總會被最先執行,而且每個Task只會被執行一遍,每次構建的依賴關係是在構建的配置階段確定的,在gradle構建中,構建的生命週期主要包括以下三個階段:

- **初始化(Initialization)**

構建工具會根據每個build.gradle檔案建立出一個Project例項,初始化階段會執行專案根目錄下的Settings.gradle檔案,來分析哪些專案參與構建
> include ':app'
include ':libraries:someProject'



- **配置(Configuration)**

這個階段通過執行構建指令碼來為每個project建立並分配Task。配置階段會去載入所有參與構建的專案的build.gradle檔案,會將build.gradle檔案例項化為一個Gradle的project物件,然後分析project之間的依賴關係,下載依賴檔案,分析project下的task之間的依賴關係<br />
<br />

- **執行(Execution)**

這是Task真正被執行的階段,Gradle會根據依賴關係決定哪些Task需要被執行,以及執行的先後順序。<br />task是Gradle中的最小執行單元,我們所有的構建,編譯,打包,debug,test等都是執行了某一個task,一個project可以有多個task,task之間可以互相依賴。例如我有兩個task,taskA和taskB,指定taskA依賴taskB,然後執行taskA,這時會先去執行taskB,taskB執行完畢後在執行taskA。

![image.png](https://cdn.nlark.com/yuque/0/2019/png/215777/1556187110722-537b15e2-6470-443b-b4d1-ac1e6bd1cbaf.png#align=left&display=inline&height=275&name=image.png&originHeight=275&originWidth=727&size=81528&status=done&width=727)<br />在根目錄和app目錄下的build.gradle中會引用下面的外掛

dependencies {
        classpath 'com.android.tools.build:gradle:2.2.2'
    }

apply plugin: 'com.android.application'

![image.png](https://cdn.nlark.com/yuque/0/2019/png/215777/1556371938551-daac116f-1a1c-4d09-bf3f-417540040dd9.png#align=left&display=inline&height=391&name=image.png&originHeight=599&originWidth=388&size=182758&status=done&width=253)
<a name="uE3w6"></a>
### 
<a name="lFk5D"></a>
### Android 三個檔案重要的gradle

Gradle專案有3個重要的檔案需要深入理解:

- settings.gradle 

settings.gradle 檔案會在構建的 initialization 階段被執行,它用於告訴構建系統哪些模組需要包含到構建過程中。對於單模組專案, settings.gradle 檔案不是必需的。對於多模組專案,如果沒有該檔案,構建系統就不能知道該用到哪些模組。

- 專案根目錄的 build.gradle 

專案根目錄的 build.gradle 檔案用來配置針對所有模組的一些屬性。它預設包含2個程式碼塊:buildscript{…}和allprojects{…}。前者用於配置構建指令碼所用到的程式碼庫和依賴關係,後者用於定義所有模組需要用到的一些公共屬性。

複製程式碼

buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:2.3.2' } }

allprojects { repositories { jcenter() } }

task clean(type: Delete) { delete rootProject.buildDir }


buildscript:定義了 Android 編譯工具的類路徑。repositories中, jCenter是一個著名的 Maven 倉庫。<br />allprojects:中定義的屬性會被應用到所有 moudle 中,但是為了保證每個專案的獨立性,我們一般不會在這裡面操作太多共有的東西。

- 模組目錄的 build.gradle 。

模組級配置檔案 build.gradle 針對每個moudle 的配置,如果這裡的定義的選項和頂層 build.gradle定義的相同。它有3個重要的程式碼塊:plugin,android 和 dependencies。

<a name="XbmTU"></a>
### 常用gradle命令

```groovy
//構建
gradlew app:clean    //移除所有的編譯輸出檔案,比如apk

gradlew app:build   //構建 app module ,構建任務,相當於同時執行了check任務和assemble任務

//檢測
gradlew app:check   //執行lint檢測編譯。

//打包
gradlew app:assemble //可以編譯出release包和debug包,可以使用gradlew assembleRelease或者gradlew assembleDebug來單獨編譯一種包

gradlew app:assembleRelease  //app module 打 release 包

gradlew app:assembleDebug  //app module 打 debug 包

//安裝,解除安裝

gradlew app:installDebug  //安裝 app 的 debug 包到手機上

gradlew app:uninstallDebug  //解除安裝手機上 app 的 debug 包

gradlew app:uninstallRelease  //解除安裝手機上 app 的 release 包

gradlew app:uninstallAll  //解除安裝手機上所有 app 的包

複製程式碼

監聽生命週期

在gradle的構建過程中,gradle為我們提供了非常豐富的鉤子,幫助我們針對專案的需求定製構建的邏輯,如下圖所示:

image.png

要監聽這些生命週期,主要有兩種方式:

  • 新增監聽器
  • 使用鉤子的配置塊

關於可用的鉤子可以參考GradleProject中的定義,常用的鉤子包括:

Project

Project提供的生命週期回撥方法有

//在 Project 進行配置前呼叫
void beforeEvaluate(Closure closure)
//在 Project 配置結束後呼叫
void afterEvaluate(Closure closure)
複製程式碼

beforeEvaluate 必須在父模組的 build.gradle 對子模組進行配置才能生效,因為在當前模組的 build.gradle 中配置,它自己本身都沒配置好,所以不會監聽到。

settings.gradle 程式碼:

include ":app"
複製程式碼

build.gradle 程式碼:

//對子模組進行配置
subprojects { sub ->
    sub.beforeEvaluate { proj ->
        println "子專案beforeEvaluate回撥..."
    }
}
println "根專案配置開始---"
task rootTest {
    println "根專案裡任務配置---"
    doLast {
        println "執行根專案任務..."
    }
}
println "根專案配置結束---"
複製程式碼

app/build.gradle 程式碼:

println "APP子專案配置開始---"
afterEvaluate {
    println "APP子專案afterEvaluate回撥..."
}
task appTest {
    println "APP子專案裡任務配置---"
    doLast {
        println "執行子專案任務..."
    }
}
println "APP子專案配置結束---"
複製程式碼

在根目錄執行:gradle -q,結果如下:

根專案配置開始---
根專案裡任務配置---
根專案配置結束---
子專案beforeEvaluate回撥...
APP子專案配置開始---
APP子專案裡任務配置---
APP子專案配置結束---
APP子專案afterEvaluate回撥...
複製程式碼

project.android 獲取到AppExtension:

Gradle

Gradle 提供的生命週期回撥方法很多,部分與 Project 裡的功能雷同:

//在project進行配置前呼叫,child project必須在root project中設定才會生效,root project必須在settings.gradle中設定才會生效
void beforeProject(Closure closure)
//在project配置後呼叫
afterProject(Closure closure)
//構建開始前呼叫
void buildStarted(Closure closure)
//構建結束後呼叫
void buildFinished(Closure closure)
//所有project配置完成後呼叫
void projectsEvaluated(Closure closure)
//當settings.gradle中引入的所有project都被建立好後呼叫,只在該檔案設定才會生效
void projectsLoaded(Closure closure)
//settings.gradle配置完後呼叫,只對settings.gradle設定生效
void settingsEvaluated(Closure closure)
複製程式碼
  • beforeProject()/afterProject()
    等同於Project中的beforeEvaluateafterEvaluate
  • settingsEvaluated()
    settings指令碼被執行完畢,Settings物件配置完畢
  • projectsLoaded()
    所有參與構建的專案都從settings中建立完畢
  • projectsEvaluated()
    所有參與構建的專案都已經被評估完

我們修改 setting.gradle 的程式碼如下:

gradle.settingsEvaluated {
    println "settings:執行settingsEvaluated..."
}
gradle.projectsLoaded {
    println "settings:執行projectsLoaded..."
}
gradle.projectsEvaluated {
    println "settings: 執行projectsEvaluated..."
}
gradle.beforeProject { proj ->
    println "settings:執行${proj.name} beforeProject"
}
gradle.afterProject { proj ->
    println "settings:執行${proj.name} afterProject"
}
gradle.buildStarted {
    println "構建開始..."
}
gradle.buildFinished {
    println "構建結束..."
}
include ":app"
複製程式碼

這個時候的執行結果如下:

settings:執行settingsEvaluated...
settings:執行projectsLoaded...
settings:執行test beforeProject
根專案配置開始---
根專案裡任務配置---
根專案配置結束---
settings:執行test afterProject
settings:執行app beforeProject
子專案beforeEvaluate回撥...
APP子專案配置開始---
APP子專案裡任務配置---
APP子專案配置結束---
settings:執行app afterProject
APP子專案afterEvaluate回撥...
settings: 執行projectsEvaluated...
構建結束...
複製程式碼

可以看到 gradle.beforeProject 與 project.beforeEvaluate 是類似的,同樣 afterProject 與 afterEvaluate 也是類似的。

除此之外,Gradle 還有一個通用的設定生命週期監聽器的方法:addListener

image.png

上面的 BuildListener、ProjectEvaluationListener 等與前面的部分 API 功能是一致的,這裡不再贅述了。

TaskExecutionGraph(Task執行圖)

Gradle 在配置完成後,會對所有的 task 生成一個有向無環圖,這裡叫做 task 執行圖,他們決定了 task 的執行順序等。同樣,Gradle 可以對 task 的執行生命週期進行監聽。

//任務執行前掉用
void afterTask(Closure closure)
//任務執行後呼叫
void beforeTask(Closure closure)
//所有需要被執行的task已經task之間的依賴關係都已經確立
void whenReady(Closure closure)
複製程式碼

通過 gradle.getTaskGraph() 方法來獲取 task 執行圖:

TaskExecutionGraph taskGraph = gradle.getTaskGraph()
taskGraph.whenReady {
    println "task whenReady"
}
taskGraph.beforeTask { Task task ->
    println "任務名稱:${task.name} beforeTask"
}
taskGraph.afterTask { Task task ->
    println "任務名稱:${task.name} afterTask"
}
複製程式碼

生命週期回撥的執行順序:

gradle.settingsEvaluated->
gradle.projectsLoaded->
gradle.beforeProject->
project.beforeEvaluate->
gradle.afterProject->
project.afterEvaluate->
gradle.projectsEvaluated->
gradle.taskGraph.graphPopulated->
gradle.taskGraph.whenReady->
gradle.buildFinished
複製程式碼

4、自定義外掛開發

三種方式

型別 說明
Build script 把外掛寫在 build.gradle 檔案中,一般用於簡單的邏輯,只在該 build.gradle 檔案中可見
buildSrc 專案 將外掛原始碼放在 rootProjectDir/buildSrc/src/main/groovy 中,只對該專案中可見,適用於邏輯較為複雜
獨立專案 一個獨立的 Groovy 和 Java 專案,可以把這個專案打包成 Jar 檔案包,一個 Jar 檔案包還可以包含多個外掛入口,將檔案包釋出到託管平臺上,供其他人使用。本文將著重介紹此類。

具體從外掛開發可以參考

需要注意的是 在main目錄下建立
1、resources/META-INF/gradle-plugins資料夾,
2、在gradle-plugins資料夾下建立一個xxx.properties檔案,(com.learntransform.testtransform.properties)
注意:這個xxx就是在app下的build.gradle中引入時的名字,例如:apply plugin: ‘xxx’(apply plugin:'com.learntransform.testtransform'
3、在檔案書寫引用到外掛 implementation-class=me.xsfdev.learntransform.Hotfix

image.png

外掛的本地化

  • 本地外掛module


group = 'com.learntranform'
version = '1.0.1'
uploadArchives {
    repositories {
        flatDir {
            name "localRepository"
            dir "../app/localRepository/libs"
        }
    }
}
複製程式碼
  • 工程的gradle

buildscript {
    ext.kotlin_version = '1.2.41'
    repositories {
        flatDir {
            name 'localRepository'
            dir "app/localRepository/libs"
        }
        mavenLocal()
        jcenter()
        google()
    }
    dependencies {
        classpath(group: 'com.plugintest', name: 'hellodsl', version: '1.0.0') {
            changing = true
        }
        classpath(group: 'com.learntransform', name: 'learntransform', version: '1.0.1') {
            changing = true
        }
        classpath 'com.android.tools.build:gradle:3.1.2'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'com.novoda:bintray-release:0.8.0' //jcenter新增
        classpath 'com.xsfdev:complexcriptdsl:1.0.0'
    }
}
複製程式碼

完整程式碼可以參考 LearnGradle

5、常用方法

android.applicationVariants

更多參考需要看原始碼

android.applicationVariants 返回的是

  public DomainObjectSet<ApplicationVariant> getApplicationVariants() {
        return applicationVariantList;
   }
複製程式碼

一層層追 相關的有

image.png

在BaseVariant中
image.png

Task#upToDateWhen

每次都會編譯

outputs.upToDateWhen { true } doesn't mean "the task is up-to-date." It just means that the outputs are up-to-date for that particular spec. Gradle will still do its own up-to-date checks.
The other thing that may be confusing is where the task's actions are defined. If the actions are defined in the build script, the build script itself is an input to the task. So changes to the build script will make the task out-of-date.
So if I had a task like:

task myTask {
    def outputFile = file("output.txt")
    outputs.file outputFile
    doLast {
        outputFile.text = "Done"
    }
    outputs.upToDateWhen { false }
}
複製程式碼

Whenever I run this, myTask is out-of-date. If I switch the false to true, the first time I run it, the task is out-of-date still (because the buildscript changed). When I run it again, it would be up-to-date (all inputs are the same). You'll see this at --info level logging.

Failed to notify project evaluation listener

The versions of the Android Gradle plugin and Gradle are not compatible.

參考

相關文章