做爲Android開發你必須明白的Gradle基礎

做爲一個Android開發程序員,若是你的build.gradle都只能靠IDE生成或者從別的項目中複製粘貼來完成,那麼你該好好的看完這篇文章,掌握一下你不知道的Gradle基礎。html

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

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

  • 強大的DSL和豐富的gradle的API
  • gradle就是groovy
  • 強大的依賴管理
  • 可拓展性
  • 與其餘構建工具的集成

三種構建腳本

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

腳本類型 委託對象
Init script Gradle
Settings script Settings
Build script Project

init.gradle

對應的就是上面的Init script,實際上就是Gradle對象的委託,因此在這個init 腳本中調用的任何屬性引用以及方法,都會委託給這個 Gradle 實例。程序員

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

配置Init scrip的依賴

每一個腳本的執行均可以配置當前腳本自己執行所須要的依賴項。Init scrip的配置以下:spring

// initscript配置塊包含的內容就是指當前腳本自己的執行所須要的配置
// 咱們能夠在其中配置好比依賴路徑等等
initscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath group: 'org.apache.commons', name: 'commons-math', version: '2.0'
    }
}
複製代碼

使用Init scrip

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

  • 在執行gradle命令的時候,經過-I--init-script命令選項指定腳本的路徑api

    這種方式能夠針對具體的一次構建。tomcat

  • 把一個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對象的類圖:

每個經過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對象的類圖以下所示:

項目配置

在build.gradle腳本文件中,咱們不只能夠對單獨project進行配置,也能夠定義project塊的共有邏輯等,參考下面的定義。

常見的例子好比:

// 爲全部項目添加倉庫源配置
allprojects {
    repositories {
        jcenter()
        google()
    }
}
// 爲全部子項目添加mavenPublish的配置塊
subprojects {
    mavenPublish {
        groupId = maven.config.groupId
        releaseRepo = maven.config.releaseRepo
        snapshotRepo = maven.config.snapshotRepo
    }
}
複製代碼

Task

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

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爲咱們提供了很是豐富的鉤子,幫助咱們針對項目的需求定製構建的邏輯,以下圖所示:

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

  • 添加監聽器
  • 使用鉤子的配置塊

關於可用的鉤子能夠參考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的依賴配置管理以下所示:

自定義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的使用和問題歡迎一塊兒討論。

相關文章
相關標籤/搜索