作為Android開發你必須明白的Gradle基礎

karspb發表於2021-09-09

作為一個Android開發程式設計師,如果你的build.gradle都只能靠IDE生成或者從別的專案中複製貼上來完成,那麼你該好好的看完這篇文章,掌握一下你不知道的Gradle基礎。

文中的圖片均來自於網路,侵刪

Gradle是一個基於JVM的構建工具,目前Android Studio中建立的工程都是基於gradle進行構建的。Gradle的與其他構建工具(ant、maven)的特性主要包括:

  • 強大的DSL和豐富的gradle的API
  • gradle就是groovy
  • 強大的依賴管理
  • 可擴充性
  • 與其他構建工具的整合

三種構建指令碼

Gradle的指令碼都是配置型指令碼。每一種指令碼型別實際上都是某個具體的gradle的API中的類物件的委託,指令碼執行對應的其實是其委託的物件的配置。在一個完整的gradle的構建體系中,總共有三種型別的構建指令碼,同時也分別對應著三種委託物件

指令碼型別 委託物件
Init script Gradle
Settings script Settings
Build script Project

init.gradle

對應的就是上面的Init script,實際上就是Gradle物件的委託,所以在這個init 指令碼中呼叫的任何屬性引用以及方法,都會委託給這個 Gradle 例項。

Init script的執行發生在構建開始之前,也是整個構建最早的一步。

配置Init scrip的依賴

每個指令碼的執行都可以配置當前指令碼本身執行所需要的依賴項。Init scrip的配置如下:

// initscript配置塊包含的內容就是指當前指令碼本身的執行所需要的配置
// 我們可以在其中配置比如依賴路徑等等
initscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath group: 'org.apache.commons', name: 'commons-math', version: '2.0'
    }
}
複製程式碼

使用Init scrip

要使用一個定義好的Init scrip,主要有以下幾個方式

  • 在執行gradle命令的時候,通過-I--init-script命令選項指定指令碼的路徑

    這種方式可以針對具體的一次構建。

  • 把一個init.gradle檔案放到 *USER_HOME*/.gradle/ 目錄

  • 把一個檔名以.gradle結尾的檔案放到Gradle 分發包*GRADLE_HOME*/init.d/ 目錄內

    以上的兩種方式是全域性的,對機器內的構建都會起作用

settings.gradle

對應的是Settings script指令碼型別,是Settings物件的委託。在 指令碼中呼叫的任何屬性引用以及方法,都會委託給這個 Settings 例項。

Settings script的執行發生在gradle的構建生命週期中的初始化階段。Settings指令碼檔案中宣告瞭構建所需要的配置,並用以例項化專案的層次結構。在執行settings指令碼並初始化Settings物件例項的時候,會自動的構建一個根專案物件rootProject並參與到整個構建當中。(rootProject預設的名稱就是其資料夾的名稱,其路徑就是包含setting指令碼檔案的路徑)。

下面是一張關於Settings物件的類圖:

作為Android開發你必須明白的Gradle基礎

每一個通過include方法被新增進構建過程的project物件,都會在settings指令碼中創造一個ProjectDescriptor的物件例項。

因此,在settings的指令碼檔案中,我們可以訪問使用的物件包括:

  • Settings物件
  • Gradle物件
  • ProjectDescriptor物件

獲取settings檔案

在gradle中,只要根專案/任何子專案的目錄中包含有構件檔案,那麼就可以在相應的位置執行構建。而判斷一個構建是否是多專案的構建,則是通過尋找settings指令碼檔案,因為它指示了子專案是否包含在多專案的構建中。

查詢settings檔案的步驟如下:

  1. 在與當前目錄同層次的master目錄中搜尋setting檔案
  2. 如果在1中沒有找到settings檔案,則從當前目錄開始在父目錄中查詢settings檔案。

當找到settings檔案並且檔案定義中包含了當前目錄,則當前目錄就會被認為是多專案的構建中的一部分。

build.gradle

對應的就是前面提到的Build script指令碼型別,是gradle中Project物件的委託。在指令碼中呼叫的任何屬性引用以及方法,都會委託給這個 Project 例項。

配置指令碼依賴

在build.gradle檔案中有一個配置塊buildScipt{}是用於配置當前指令碼執行所需的路徑配置等的(與initScript形似)。

buildscript {
	// 這裡的repositories配置塊要與Project例項當中的repositories區分開來
	// 這裡的repositories配置是指指令碼本身依賴的倉庫源,其委託的物件實際上是ScriptHandler
    repositories {
        mavenLocal()
        google()
        jcenter()
    }
    // 與前面的repositories配置塊相同,也要與Project當中的dependencies配置塊區分開來
    dependencies {
        classpath 'com.android.tools.build:gradle:3.1.2'
    }
}
複製程式碼

這裡補充關鍵的一點,在build.gradle檔案中,不管buildScript{}配置塊被放在哪個位置,它總是整個指令碼檔案中最先被執行的

三個構建塊

每個gradle構建都包含三個基本的構件塊:

  • project
  • task
  • property

每個構建包含至少一個project,進而又包含一個或者多個task。project和task暴露的屬性(property)可以用來控制構建。

Project

我們對project的理解更多來源於專案目錄中的build.gradle檔案(因為它其實就是project物件的委託)。Project物件的類圖如下所示:

作為Android開發你必須明白的Gradle基礎

專案配置

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

作為Android開發你必須明白的Gradle基礎

常見的例子比如:

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

Task

任務是gradle構建的基礎配置塊之一,gradle的構建的執行就是task的執行。下面是task的類圖。

作為Android開發你必須明白的Gradle基礎

task的配置和動作

當我們定一個一個task的時候,會包含配置和動作兩部分的內容。比如下面的程式碼示例:

task test{
    println("這是配置")
    
    doFirst{
        // do something here
    }
    doLast(){
        // do something here
    }
}
複製程式碼

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

  • doFirst
  • doLast

這些動作是在gradle的構建生命週期中的執行階段被呼叫。值得注意的是,一個task可以宣告多個doFirstdoLast動作。也可以為一些已有的外掛中定義的task新增動作。比如:

// 為test任務新增一個doLast的動作
test.doLast{
    // do something here
}
複製程式碼

在task的定義之中,除了動作塊以外的是配置塊,我們可以宣告變數、訪問屬性、呼叫方法等等。這些配置塊的內容發生在gradle的構建生命週期中的配置階段。因此task中的配置每次都會被執行。(動作塊只有在實際發生task的呼叫的時候才會執行)。

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的型別

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

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

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

Property

屬性是貫穿在gradle構建始終的,用於幫助控制構建邏輯的存在。gradle中宣告屬性主要有以下兩種方式:

  • 使用ext名稱空間定義擴充屬性
  • 使用gradle屬性檔案gradle.properties定義屬性

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)
        }
    }
}
複製程式碼

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

構建生命週期

前面提及到了gradle中多種指令碼型別,並且他們都在不同的生命週期中被執行。

三個階段

在gradle構建中,構建的生命週期主要包括以下三個階段:

  • 初始化(Initialization)

    如前文所述,在這個階段,settings指令碼會被執行,從而Gradle會確認哪些專案會參與構建。然後為每一個專案建立 Project 物件。

  • 配置(Configuration)

    配置 Initialization 階段建立的Project 物件,所有的配置指令碼都會被執行。(包括Project中定義的task的配置塊也都會被執行)

  • 執行(Configuration)

    這個階段Gradle會確認哪些在 Configuration 階段建立和配置的 Task 會被執行,哪些 Task會被執行取決於gradle命令的引數以及當前的目錄,確認之後便會執行

監聽生命週期

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

作為Android開發你必須明白的Gradle基礎

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

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

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

Gradle

  • beforeProject()/afterProject()

    等同於Project中的beforeEvaluateafterEvaluate

  • settingsEvaluated()

    settings指令碼被執行完畢,Settings物件配置完畢

  • projectsLoaded()

    所有參與構建的專案都從settings中建立完畢

  • projectsEvaluated()

    所有參與構建的專案都已經被評估完

TaskExecutionGraph

  • whenReady()

    task圖生成。所有需要被執行的task已經task之間的依賴關係都已經確立

Project

  • beforeEvaluate()
  • afterEvaluate()

依賴管理

在前面提及的Gradle的主要特性之中,其中的一點就是強大的依賴管理。Gradle中具備豐富的依賴型別,相容多種依賴倉庫。同時Gradle中的每一項依賴都是基於特定的範圍(scope)進行分組管理的。

在gradle中新增為專案新增依賴的方式如下所示:

// build.gradle

// 新增依賴倉庫源
repositories {
    google()
    mavenCentral()
}
// 新增依賴
// 依賴型別包括:檔案依賴、專案依賴、模組依賴
dependencies {
    // local dependencies.
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    ...
}
複製程式碼

四種依賴型別

Gradle中的依賴型別有四類:

  • 模組依賴

    這是gradle中比較常見的依賴型別, 它通常指向倉庫中的一個構件,如下所示:

    dependencies {
        runtime group: 'org.springframework', name: 'spring-core', version: '2.5'
        runtime 'org.springframework:spring-core:2.5',
                'org.springframework:spring-aop:2.5'
        runtime(
            [group: 'org.springframework', name: 'spring-core', version: '2.5'],
            [group: 'org.springframework', name: 'spring-aop', version: '2.5']
        )
        runtime('org.hibernate:hibernate:3.0.5') {
            transitive = true
        }
        runtime group: 'org.hibernate', name: 'hibernate', version: '3.0.5', transitive: true
        runtime(group: 'org.hibernate', name: 'hibernate', version: '3.0.5') {
            transitive = true
        }
    }
    複製程式碼

    模組依賴對應於gradle的API中的 ExternalModuleDependency物件

  • 檔案依賴

    dependencies {
        runtime files('libs/a.jar', 'libs/b.jar')
        runtime fileTree(dir: 'libs', include: '*.jar')
    }
    複製程式碼
  • 專案依賴

    dependencies {
        compile project(':shared')
    }
    複製程式碼

    專案依賴對應於gradle的API中的 ProjectDependency物件

  • 特定的Gradle發行版依賴

    dependencies {
        compile gradleApi()
        testCompile gradleTestKit()
        compile localGroovy()
    }
    複製程式碼

管理依賴配置

gradle中專案的每一項依賴都是應用於一個特定的範圍的,在gradle中用 Configuration物件表示。每一個Configuration物件都會有一個唯一的名稱。Gradle的依賴配置管理如下所示:

作為Android開發你必須明白的Gradle基礎

自定義Configuration

在gradle中,自定義Configuration物件是非常簡單的,同時定義自己的Configuration物件的時候,也可以繼承於已有的Configuration物件,如下所示:

configurations {
    jasper
    // 定義繼承關係
    smokeTest.extendsFrom testImplementation
}

repositories {
    mavenCentral()
}

dependencies {
    jasper 'org.apache.tomcat.embed:tomcat-embed-jasper:9.0.2'
}

複製程式碼

管理傳遞性依賴

在實際的專案依賴管理中存在這樣的一種依賴關係:

  • 模組b依賴於模組c
  • 模組a依賴於模組b
  • 模組c成為了模組a的傳遞依賴

在處理上面這種傳遞性依賴的時候,gradle提供了強大的管理功能

使用依賴約束

依賴約束可以幫助我們控制傳遞性依賴以及自身的依賴的版本號(版本範圍),比如:

dependencies {
    implementation 'org.apache.httpcomponents:httpclient'
    constraints {
        // 這裡httpclient是專案本身的依賴
        // 這個約束表示,不管是專案本身的依賴是還是傳遞依賴都強制使用這個指定的版本號
        implementation('org.apache.httpcomponents:httpclient:4.5.3') {
            because 'previous versions have a bug impacting this application'
        }
        // commons-codec並沒有被宣告為專案本身的依賴
        // 所以僅當commons-codec是傳遞性依賴的時候這段邏輯才會被觸發
        implementation('commons-codec:commons-codec:1.11') {
            because 'version 1.9 pulled from httpclient has bugs affecting this application'
        }
    }
}
複製程式碼

排除特定的傳遞性依賴

有的時候,我們所依賴的專案/模組會引入多個傳遞性依賴。而其中部分的傳遞性依賴我們是不需要的,這時候可以使用exclude排除部分的傳遞性依賴,如下所示:

dependencies {
    implementation('log4j:log4j:1.2.15') {
        exclude group: 'javax.jms', module: 'jms'
        exclude group: 'com.sun.jdmk', module: 'jmxtools'
        exclude group: 'com.sun.jmx', module: 'jmxri'
    }
}
複製程式碼

強制使用指定的依賴版本

Gradle通過選擇依賴關係圖中找到的最新版本來解決任何依賴版本衝突。 可是有的時候,某些專案會需要使用一個較老的版本號作為依賴。這時候我們可以強制指定某一個版本。例如:

dependencies {
    implementation 'org.apache.httpcomponents:httpclient:4.5.4'
    // 假設commons-codec的最新版本是1.10
    implementation('commons-codec:commons-codec:1.9') {
        force = true
    }
}
複製程式碼

要注意的是,如果依賴專案中使用了新版本才有的api,而我們強制使用了舊版本的傳遞依賴之後,會引起執行時的錯誤

禁止傳遞性依賴

dependencies {
    implementation('com.google.guava:guava:23.0') {
        transitive = false
    }
}
複製程式碼

依賴關係解析

使用依賴關係解析規則

依賴關係解析規則提供了一種非常強大的方法來控制依賴關係解析過程,並可用於實現依賴管理中的各種高階模式。比如:

  • 統一構件組的版本

    很多時候我們依賴一個公司的庫會包含多個module,這些module一般都是統一構建、打包和釋出的,具備相同的版本號。這個時候我們可以通過控制依賴關係的解析過程做到版本號統一。

    configurations.all {
        resolutionStrategy.eachDependency { DependencyResolveDetails details ->
            if (details.requested.group == 'org.gradle') {
                details.useVersion '1.4'
                details.because 'API breakage in higher versions'
            }
        }
    }
    複製程式碼
  • 處理自定義的版本scheme

    configurations.all {
        resolutionStrategy.eachDependency { DependencyResolveDetails details ->
            if (details.requested.version == 'default') {
                def version = findDefaultVersionInCatalog(details.requested.group, details.requested.name)
                details.useVersion version.version
                details.because version.because
            }
        }
    }
    
    def findDefaultVersionInCatalog(String group, String name) {
        //some custom logic that resolves the default version into a specific version
        [version: "1.0", because: 'tested by QA']
    }
    複製程式碼

關於更多依賴關係解析規則的使用例項可以參考gradle的API中的 ResolutionStrategy

使用依賴關係的替代規則

依賴關係的替換規則和上面的依賴關係解析規則有點相似。實際上,依賴關係解析規則的許多功能可以通過依賴關係替換規則來實現。依賴關係的替換規則允許專案依賴(Project Dependency)和模組依賴(Module Dependency)被指定的替換規則透明地替換。

// 使用專案依賴替換模組依賴
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute module("org.utils:api") with project(":api") because "we work with the unreleased development version"
        substitute module("org.utils:util:2.5") with project(":util")
    }
}
// 使用模組依賴替換專案依賴
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute project(":api") with module("org.utils:api:1.3") because "we use a stable version of utils"
    }
}
複製程式碼

除了上面兩種之外,還有其他三種的依賴關係規則處理。因為沒有實際使用過,這裡不過多闡述,想了解更多可以檢視官方的文件Customizing Dependency Resolution Behavior

  • 使用元件後設資料(meta-data)規則
  • 使用元件選擇規則
  • 使用模組更換規則

外掛開發

外掛開發是gradle靈活的構建體系中的一個強大工具。通過gradle中的PluginAPI,我們可以自定義外掛,把一些通用的構建邏輯外掛化並廣泛的運用。比如Android專案中都會使用的:com.android.application,kotlin-android,java等等。

網上關於外掛開發的文章已經很多,這裡不再贅述。這裡推薦我寫的一個Gradle外掛,也是在我完全看了gradle的官方文件之後,結合前面提及到的依賴管理的知識寫的:

  • EasyDependency

    一個幫助提高元件化開發效率的gradle外掛,提供的功能包括:

    1. 釋出模組的構件都遠端maven倉庫
    2. 動態更換依賴配置:對模組使用原始碼依賴或者maven倉庫的構件(aar/jar)依賴

寫在最後

全文基本是在看了gradle的官方文件及相關資料之後,按照自己的思路的整理和總結。關於gradle的使用和問題歡迎一起討論。

相關文章