Gradle 學習之路

Gradle 學習之路

封面圖
封面圖

前言

雖然從開始用 Android Studio 開發 Android 應用就一直在接觸 Gradle,但對 Gradle 始終都有一些陌生感,表如今平常的開發中就是不敢隨便改 build.gradle 文件,一旦 sync 出錯,只會複製錯誤找谷歌,但是解決方案也不必定可以完美解決本身的問題。還有就是不熟悉 Gradle 的時候,也不能很好的理解整個項目的配置,畢竟 Gradle 是 Android 項目的構建腳本。html

每當我想好好的學習一下 Gradle,老是被從哪裏開始這個問題所戰勝。直到有一天…… 我終於不怵它了,而後又過了好久我決定寫下這篇文章。java

這篇文章目的主要目的在於回顧個人 Gradle 學習之路,若是能對你有一些幫助,那定是極好的。android

目錄

Gradle

Gradle 是一個用 Java 語言開發的構建工具,目前最新版本是 6.5。對於 Android 開發者來講,最多見的就是在 Android 開發中使用 Gradle,實際上 Gradle 還能夠用於 Java 應用或 Java Web 應用等項目進行開發。git

  1. 筆者開始寫下這篇筆記是在 2020 年 6 月 8 日。最新版本可在 Gradle 官網進行查看。
  2. 聽說有些 Java 開發者從 Maven 遷移到到 Gradle 以後就再也回不去了。

Gradle User Home

就像安裝 Java 的 JDK 同樣,Gradle 也有本身的 User Home 目錄,通常在這個目錄中存放着一些全局的配置信息。在 Mac 下的路徑爲:user/.gradlegithub

GradleUserHome
GradleUserHome

在 Gradle User Home 中咱們主要來介紹 wrapper 這個文件夾。wrapper 內包含 dists 文件夾,dists 就是 distribution 的縮寫。web

distribution

剛纔咱們說 Gradle 如今發佈到了 6.5 ,不過 6.5 這個版本內部也有不一樣的類型。Gradle 將每個版本分爲了三個類型,它們分別是:api

  • src緩存

    源代碼類型服務器

  • binapp

    源代碼打包後的執行文件

  • all

    包含 bin 及一些示例代碼和相關文檔。

這裏的 distribution 就是每一個由 Gradle 構建的項目中 gradle-wrapper.properties 文件配置的一部分,以下圖。

gradle-wrapper.properties
gradle-wrapper.properties

在 Gradle User Home 中的 wrapper 下的 dists 文件夾內存放的其實就是各個版本的的不一樣類型的 Gradle 下載並解壓後的文件。

GradleUserHomeWrapperDists
GradleUserHomeWrapperDists

仔細看這張圖底部的文件路徑 jiang/.gradle/wrapper/dists,這裏隱藏了 2 個問題:

  1. 你看個人 dists 文件夾內有從 4.1 到 6.4 的各類版本的包,那麼平常開發中,不是隻用一個版本的就能夠了嗎?爲何要保存這麼多版本在本地?
  2. 既然是下載下來的 Gradle 版本,那爲何 dists 是在 wrapper 文件夾下面呢?

我先回答第一個問題。是由於多版本,具體來講就是,Gradle 支持本地存在多個版本,而且這些版本之間相互獨立。舉個例子,你在去年用 Android Studio 開發 App 用的是 Gradle 4.1 以後因爲各類緣由,沒有及時對它進行升級。今年你又新建立了一個項目,這個新建立的項目用的是最新的 6.5。你不能由於有了新項目,就不讓老項目不能運行吧,那你也太渣了。

wrapper

在瞭解爲何會存在多版本以後,咱們能夠來討論一下爲何 dists 是在 wrapper 文件夾而不是直接在 Gradle User Home 的根目錄下。

那是由於,咱們在使用 Gradle 構建項目的時候通常不直接使用 dists 裏的各類具體版本進行構建,而是選用 wrapper 進行。經過使用 wrapper 進行查找並使用具體的 Gradle 版本完成構建任務。

具體來講,當咱們去構建項目時,首先是根據當前項目中的 gradle-wrapper.properties 文件中設置的目標版本及下載地址。若是在本地「User Home 的 wrapper 下的 dists 文件夾」找不到,就會從服務器上進行下載,這樣既能保證在一臺從未使用過 Gradle 的機器上運行 Gradle 構建工具,也能保證了多項目之間的 Gradle 各版本相互分離,互不干擾。

Gradle-Wrapper
Gradle-Wrapper

圖片來自 Gradle 官網的 Gradle Wrapper 介紹。

Gradle 項目

如今咱們來建立一個最簡單的項目來認識一下 Gradle 項目。

gradle-init
gradle-init
  1. 執行 gradle init 可能須要正在閱讀的你在你的機器上配置 gradle 的路徑到環境變量中;
  2. 截圖中的紅色框 ①、②、③ 是 gradle 提示讓開發者輸入的項目相關的配置信息。
  3. 截圖中的紅色框 ④ 是直接完 gradle init 後的目錄結構。

能夠發現裏面有 gradlew 和 gradlew.bat 這兩個文件,而這兩個文件就是咱們當前這個項目執行構建是所必須的的腳本文件。文件名 gradlew 其實就是 gradle wrapper 的縮寫。gradlew 文件是用在 macOS 和 Linux 上的執行腳本,gradlew 則是運行在 Windows 上的。

如今咱們來運行一下看看,執行 ./gradlew help 命令看看

gradlew help
gradlew help

注意看這個 GIF,在開始的時候,提示了一段話。

Starting a Gradle Daemon, 3 stopped Daemons could not be reused, use --status for details

什麼意思呢?就是說,啓動了一個 Gradle Daemon,有 3 個已經被中止的 Daemon 是不能被使用的,詳情使用 ./gradlew --status 進行查看。

什麼是 Daemon?我只是想執行 help 命令,怎麼還扯上了 Daemon?

是由於雖然你執行的是 ./gradlew help 實際上,gradle 經過將這條命令轉發給了一個叫 Daemon 的進程,由它在完成開發者所期待的指令。

Daemon

Gradle Daemon 是 Gradle 在 3.0 以後新加入的一個功能,旨在加快項目的構建速度。Daemon 是做爲 Gradle 的一個後臺進程,在這個後臺進程中會緩存參與構建的項目的目錄結構、文件、Tasks 還有一些其餘東西在內存中。

默認狀況下 Daemon 模式是打開的,開發者能夠選擇關閉 Daemon 模式,固然若是關閉了,每構建一次項目就會建立一個 JVM 去執行,直到執行結束,關閉並釋放資源,若是頻繁的進行,其實浪費的是開發者的時間。Gradle 官方也是建議咱們使用 Daemon。

以前說,我機器上能夠同時存在多個版本的 Gradle,那若是我正好又同時構建了多個項目,Daemon 又是什麼表現呢?

事實上,當開發者想要執行一個 gradle 指令時,會先去查找有沒有可用的 Daemon,這裏的能夠指的是沒有被關閉,同時知足執行構建的所指定的參數。查找到可用的 Daemon 以後就會交給 Daemon 去執行,找不到就根據當前的構建請求去啓動一個 Daemon。

執行構建所指定的參數?什麼東西?我沒指定過啊,簡單來講,就是你要完成構建的環境,好比 Java 的版本或 minimum heap size 等等。

gradle-download-wrapper-run
gradle-download-wrapper-run

看這個圖,我複製了剛纔建立的 gradle 項目而後改了 gradle/wrapper/gradle-wrapper.properties 中指定的 Gradle 版本,將原來的 5.4.1-all 改成了 6.5-all,而後執行 ./gradlew help 指令。

Gradle 在執行時就是先下載 6.5-all 的文件,以後啓動 Daemon 並執行 help 指令。

假設啓動了多個不一樣版本的 Daemon,會不會特別佔用內存或是我長時間不進行構建不就浪費內存了嗎?這個其實也不用怎麼操心。Gradle Daemon 在 3 小時以內沒有使用,就會被關閉,下次再有構建請求,會從新開啓 Daemon,並且當系統內存不足時 Daemon 也會被清理。

使用 ./gradlew --stop 就能手動中止當前的使用的 Daemon 進程。

小結

Gradle 是一個使用 Java 開發的構建工具,它有不一樣的版本、每一個版本之間又有不一樣的類型,當執行一個 Gradle 指令時,其實是經過 Gradle Daemon 來進行的,Daemon 會在 3 小時內無操做時自動關閉節約內存。

構建

直到如今,咱們好像還沒跟構建項目打交道,說的全是 Gradle 結構相關的內容。下面咱們就來看看若是使用 Gradle 進行項目構建。

在開始講構建以前,先看一個小例子。輸出一些文字

打開 build.gradle 文件,在其中加上一行輸入。

println(" I'm first line in build.gradle ")
複製代碼

問題來了,我該怎麼運行讓他顯示出來呢?答案是無法經過像運行 Java 程序那樣運行,而後輸出這行文字。不過 Gradle 有他本身的一套規範。

Project&Task

Project 和 Task 都是 Gradle 中的模型,它們兩個的存在構成了 Gradle 運行所必備的項目結構,一個 Gradle 項目可能包含多個 Project 同時一個 Project 中可能包含多個 Task。真正執行構建任務的實際上就是執行各類的 Task,而且 Task 之間能夠相互依賴,經過組合的形式能夠完成諸多任務。

tastTree
tastTree

一個 Android 項目的 build Task 的依賴部分截圖。

一個 Gradle 項目在執行構建的過程當中,會先經過當前目錄下的 settings.gradle 來配置當前的項目結構,好比:項目名稱是什麼,包含了幾個 Project 以及每一個 Project 的路徑名稱等信息。在這以後又會根據 Project 來構建其所包含的 Task 及 Task 之間的層級關係。

Gralde-Project-Task
Gralde-Project-Task

上面這張圖列舉了 Project 和 Task 的一些屬性,完整的屬性列表請看 Project ProtertiesTask Proterties

一個基本的 Gradle 項目也有一些默認的 Task,好比以前咱們 以前執行的 help 就是一個輔助功能的 Task,還有 tasks 用於查看當前項目全部的 Task,projects 用於查看當前項目全部的 Project。

gradlew-projects
gradlew-projects

執行 ./gradlew projects 的結果

gradlew-tasks
gradlew-tasks

執行 ./gradlew tasks 的結果

細心看的話,會發現這兩張圖片的頂部都有這樣一些字樣:

> Configure project :
 I'm first line in build.gradle
複製代碼

這是什麼意思?這行文字,不是咱們剛纔寫在 build.gradle 第一行的嗎?我沒有執行啊,我執行的明明是 projectstasks 兩個指令,爲何反而輸出了呢?這就不得不提到 Gradle 構建的生命週期了。

生命週期

Gradle 執行構建時的生命週期共有 3 個,分別爲:initialization、configuration、execution。

initialization 階段就是從當前項目讀取整個項目的配置信息,好比是否是多項目「經過在 settings.gradle 中的配置」工程、這個 Gradle 項目用到了哪些插件、項目之間的依賴關係是怎麼樣?

configuration 階段所作的任務就是把當前項目的每一個 Project 下的 build.gradle 文件從上到下依次執行一遍。在這個生命週期過程當中,也就產生了各類 Task 被建立並創建關聯,並把執行單元 Action 依次添加進各自的執行列表。

最後是 execution 階段,在這個階段就是執行在 configuration 中配置的各類的 Task。說是執行 Task,實際上是執行 Task 中的的 Actions。

在 Task 中有一些操做 Action 的方法好比:doFirst、doLast 都是添加一個實際要執行的過程,並在 execution 階段執行。

小試牛刀

println(" I'm first line in build.gradle ")
 task hello {  group('demo-run')  doLast {  println(" hello world")  } }  task("tryRun", {  group("demo-run")  dependsOn("hello")  println(" you are in configuration")   doLast {  println("You run me! in Last")  }   doFirst {  println(" You run me in First")  }.doFirst {  println(" You run me in 2nd First")  } }) 複製代碼

這段代碼是用 Groovy 寫的,意思就是建立了兩個 Task,分別是 hello 和 tryRun,它們都屬於 demo-run 這個分組,同時 tryRun 依賴於 hello。

doLast 和 doFirst 後面跟着的代碼塊就是實際要執行的內容,這個內容就叫 Action。doLast 和 doFirst 內部操做的是一個 List,doLast 至關於在這個 List 的最後添加一個 Action;doFirst 至關於插入一個 Action 做爲 List 的第一個元素,它們都是 Task 的方法。在 Gradle 生命週期的 execution 階段其實就是執行這個 Action 的 List。

gradlew-customTask
gradlew-customTask

小結

來來來,快速回顧一下,剛纔說了 Gradle 的構建模型和生命週期,模型是 Project 和 Task,單個項目中可能存在多個 Project,同時單個 Project 也可能存在多個 Task。Gradle 構建的生命週期分爲 initialization、configuration、execution 三個階段,第一個階段是讀取項目配置構建 Project,第二個階段是依次配置 Project 的中的 Task 及 Task 之間的層級關係;最後一個階段就是執行,按照 Task 的層級關係依次執行相對應 Task 中的 Actions。

說了這麼多,感受 Gradle 好像沒啥大不了啊,就像是個沒完成的功能?真的是這樣嗎?不是的,我以爲 Gradle 最大的優勢就是接下來要介紹的 Gradle Plugin。經過 Gradle Plugin 能夠完成一系列構建任務,並且幾乎每個 Android 開發者都在和 Gradle Plugin 打交道,下面咱們就一塊兒開看一下吧。

Gradle Plugin

就像前文所說的那樣,Gradle 自己並無承擔大多數任務,而是很巧妙的把任務分擔到各式各樣的 Plugin 上,這樣作的好處是,Gradle 自己不須要有特定的業務點,只須要提供一個環境給那些有須要的開發者,而有須要的人本身確定會根據去本身的業務場景去定製不一樣的功能,就像 Android 應用的構建。

  1. 說到這其實你也應該明白了,說是 Android 應用使用 Gradle 構建不夠準確,更爲準確的說法應該是,使用 Android Gradle Plugin 進行構建。
  2. 有興趣的同窗能夠自行了解 Android Gradle Plugin,這裏就不展開了。
  3. 編寫 Plugin 並非非要會 Groovy,用 Java 也能夠。

要編寫插件也不難,可能稍微有些麻煩。Gradle 考慮到開發者在不一樣場景下進行建立 Plugin,因此 Gradle 一共提供了 3 種形式 Plugin 的編寫方式。下面就示範一下其中兩種。

build.gradle 腳本 Plugin

這種方法是最簡單的 Plugin 開發方式,只須要在 build.gradle 腳本文件中添加 Plugin 的實現邏輯,並經過 apply 進行引入便可。

...
class GreetingPlugin implements Plugin<Project> {  void apply(Project project) {  project.task('helloPlugin') {  doLast {  println 'Hello from the GreetingPlugin'  }  }  } }   apply plugin: GreetingPlugin 複製代碼

實現一個 Plugin 也沒有很難,建立一個類而後實現 Plugin 接口,接口的泛型就是 Project。而後在實現 apply 方法便可。

接下來在 apply 方法內部實現本身的邏輯便可,這裏就是很簡單的建立出一個名爲 helloPlugin 的 Task,並向它的執行列表「Actions」內添加一個輸出到控制檯的一句話的 Action。

經過上文咱們能夠知道添加了一個 Task 以後,在任什麼時候候均可以執行,並且 Task 之間還能夠設置依賴關係,若是咱們的任務很複雜還能夠經過 dependsOn 方法對多個 Task 設置依賴關係。那直接建立 Task 和經過 Plugin 建立 Task 有什麼區別嗎?或者說用 Plugin 建立的 Task 有什麼優點嗎?

答案是沒有,至少在實現效果上 Plugin 實現的 Task 並無什麼優點。使用 Plugin 的目的在於開發者 Plugin 的人員指望能以最小成本讓須要的開發者接入,下降因爲人「開發者」致使問題的風險機率。在這種時候 Plugin 的優點就顯露出來了,讓開發 Plugin 的人只關注 Plugin 的開發,使用 Plugin 的人儘可能少的感知 Plugin 的存在。

我在剛用 Android Studio 開發時歷來就沒想過編譯 Android 應用的居然是另外一個工具的一個插件,這種無感知成功的。

因此這種使用腳本進行開發 Plugin 的方式就比較適合單人或小範圍內的插件開發。像 Android Gradle Plugin 這種級別的大工程都是須要一個標準項目工程來完成的,下面就來簡單介紹一下。

標準工程 Plugin

這種標準化工程開發 Plugin 與 腳本式開發的區別仍是挺大的。推薦使用 IDE 進行開發,筆者使用的是 JetBrains 開發的 IntelliJ IDEA。

  1. 我是直接將上文經過命令行建立的 gradle 導入 IDE,而後建立一個 Module 做爲開發 Plugin。項目代碼已上傳至 GitHub,文末有連接。
  2. 若是直接開發插件的話,在建立項目時,選擇使用 Gradle 做爲構建工具的進行開發便可,開發語言不受限,可以使用 Groovy 也可以使用 Java 或 Kotlin。

建立完成項目以後,第一步咱們須要添加依賴關係。依賴什麼呢?Gradle,畢竟咱們開發的是 Gradle 的 Plugin,沒有對應的環境該怎麼繼續。

在 dependencies 中添加下面這行代碼。

implementation gradleApi()
複製代碼

而後 IDE 會在右下角提示你須要同步修改。這裏選擇 import Changes 或 Enable Auto-import 都可,或是在 IDE 上找到 Gradle 面板而後點擊 ReImport All Gradle Project 。

idea-need-imported
idea-need-imported
idea-gradle-panel
idea-gradle-panel

若是沒有選擇自動 import,每次修改 .gradle 文件後須要手動 impor。後文再也不贅述。

接下來就是建立 Plugin 的入口類了,別忘記建立包路徑哦。下面是我建立的入口類,在這個 apply 方法內部就能夠編寫了。

package me.monster.gradle.plugin;
 import org.gradle.api.Plugin; import org.gradle.api.Project;  public class Entrance implements Plugin<Project> {   @Override  public void apply(Project target) {   } } 複製代碼

不過在此以前,還須要配置一個東西,一個指向性的文件。在與 java 文件夾同級目錄下建立 resources 文件夾,而後,在其中建立子文件夾及文件。路徑:resources/META-INF/gradle-plugins/{包名}.properties。在這個文件中添加一行代碼:implementation-class={入口類的路徑}。若是建立正確的話,點擊入口類名是可以跳轉到入口類的。

完成這些以後,就能在入口類中盡情的玩耍了。

自定義 Plugin 輸入

剛纔咱們在使用 build.gradle 腳本寫 Plugin 內建立了一個 Task,而後打印一句話。看起來挺死板的,讓咱們來加點自定義的輸入信息。

Object 同樣的輸入・Extension

一般咱們會把多個屬性值「字段」和方法的集合叫作對象,對應的在 Gradle 中有一種各類屬性的輸入,它就是 Extension。先來看看它長什麼樣。

userInfo {
 userName = 'little monster'  avatar = "no avatar"  age = 20 } 複製代碼

以上的這段代碼是寫在 build.gradle 文件中的。也就是在實際使用的階段的由使用者決定的輸入值。它是怎麼建立出來的呢?

首先須要定義一個明確參數及方法的對象,而後使用 project.getExtensions 方法拿到 ExtensionContainer 對象,利用這個對象的 create 方法咱們可建立出一個 Extension。

def userInfo = project.extensions.create("userInfo", UserInfo)
project.task('helloPlugin') {  doLast {  println userInfo.toString()  println 'Hello from the GreetingPlugin'  } } 複製代碼
  1. 寫在 build.gradle 中 {} 外面的名稱是由 create 方法中 name 參數控制的;
  2. 在這個例子中,我省略了 UserInfo 這個類的構造方法,使用 Java 提供的默認的無參構造,當構造方法有參數時,須要在 create 方法中的 constructionArguments 參數填入構造方法的值;

Extension 也是能夠嵌套使用的,例如:

userInfo {
 userName = 'little monster'  avatar = "no avatar"  age = 20   address {  country = "China"  province = "ShangHai"  city = "ShangHai"  } } 複製代碼

建立的嵌套 Extension 的方式也很簡單,只須要在外層 Extension 中增長一個與嵌套 Extension 同名的方法便可。具體實現方式以下:

class UserInfo {
 Address address = new Address()   /**  * 兩個 address 方法,選擇一個便可。  */  void address(Closure c) {  org.gradle.util.ConfigureUtil.configure(c, address)  }   void address(Action<Address> action) {  action.execute(address)  } } 複製代碼

更多關於 Extension 的用法及介紹這裏就不一一展開了,有興趣的朋友能夠看看官方文檔。

Map 同樣的輸入・NamedDomainObjectContainer

剛纔已經介紹了 Extension,如今來看一下 NamedDomainObjectContainer,這個名字看起來很長,可是實際的用法看起來就像 Map 的 put 方法同樣。與 put 方法相似,這種輸入方式必須有一個 name 屬性,而它本身自己則至關於 put 方法中的 value。

聽起來聽繞的,看一下具體的使用。

clothes {
 pants {  brand = "Nike"  year = 1  }   shoes {  brand = "Converse"  year = 2  } } 複製代碼

這裏的 pants、shoes 就是 name 的值。

Clothes 是一個對象,裏面有 name、brand、year 三個屬性,同時 Clothes 對象的構造方法必須有一個值賦值給 name 屬性。

class Clothes {
 /**  * 必須有 name 屬性  */  String name  String brand  int year   Clothes(String name) {  this.name = name  }   String toString() {  return getName() + " " + getBrand() + " " + getYear()  } }  class UserInfo{  /**  * UserInfo 構造方法,傳入 Project 對 clothesNamedDomainObjectContainer 進行初始化  * @param project  */  public UserInfo(Project project) {  NamedDomainObjectContainer<Clothes> domainObjs = project.container(Clothes)  clothesNamedDomainObjectContainer = domainObjs  }   /**  * 添加 Clothes  * @param action  */  void clothes(Action<NamedDomainObjectContainer<Clothes>> action) {  action.execute(clothesNamedDomainObjectContainer)  }    /**  * 打印全部 Clothes  */  public void printAllClothes() {  clothesNamedDomainObjectContainer.all { singleCloth ->  println(singleCloth.toString())  }  } } 複製代碼

使用起來也挺簡單的,不過它有什麼做用呢?它最大的亮點就在於能夠由開發者自行配置參數搭建出想要的配置而不須要額外的支持。Android 開發者應該對下面這段代碼比較熟悉。

buildTypes {
 release {  minifyEnabled true  zipAlignEnabled true  shrinkResources true  proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'  debuggable false  }   debug {  debuggable true  } } 複製代碼

buildType 就是 Android Gradle Plugin 中的一個 NamedDomainObjectContainer,其中 release、debug 只是 BuildType 的一個 Name,換句話說,你能夠再添加任意一個不與他們重名均可以完成構建,並且 Android Gradle Plugin 還會爲其生成 assemble<Name> 等配套的 Task。

狀態監聽

有些時候,開發一個插件須要在構建完成以後再進行,或是須要監控執行狀態,Gradle 也替咱們提早想好了,調用 project.getGradle 方法可以得到一個 Gradle 對象,在這個 Gradle 對象中,能夠設置各類各樣的狀態監聽,好比:buildStarted,在構建開始以後被調用,buildFinished 在構建完成以後等等。須要的朋友能夠查看 Gradle 源碼或官方文檔。

小結

這一部分咱們講了 Gradle 的 Plugin 部分,圍繞着 Plugin 的建立展開,由簡入難,先說了腳本試的插件、標準化工程的 Plugin,有了 Plugin 以後,還講了 Plugin 的輸入的兩種方式,一種爲 Extension,另外一種爲 NamedDomainObjectContainer,在最後還稍微提了一下 Gradle 這個對象中能夠設置的狀態監聽方法。

後記

Gradle 做爲 Android 開發的必備工具,一直是不少人心中的痛,有點懂,又不是那麼懂,我也是從那個階段過來的,在這裏分享一下個人 Gradle 學習之路,針對本文有什麼建議,還請指出,共同進步。

相關資料

參考來源

輔助資料

關於我

我是一個普普統統的 Android 開發者,你能夠在簡書掘金,還有個人我的博客找到我。

本文封面圖:Photo by Wengang Zhai on Unsplash

本文使用 mdnice 排版

相關文章
相關標籤/搜索