從巨集觀的角度看 Gradle 的工作過程

有風度開荒隊發表於2019-04-02

本文預設讀者使用 macOS


以我自己為例,剛開始接觸 Android 開發的時候,只是對 Java 開發有一些瞭解,對於 Android 開發的整個生態和技術棧只有一個模糊的認知。 缺乏對 Gradle 的認知,在初級開發階段可能勉強可以應付,只需要知道如何新增第三方依賴,如何調整 android block 中的配置基本上就夠了,但是隨著專案結構越來越複雜,預設提供的構建過程漸漸不能滿足開發的需求,此時就要求開發者對 Gradle 的構建過程和原理有更深入的瞭解,便於自定義個性化的構建過程。

在閱讀完 《Gradle In Action》後,我發現 Android 工程的構建過程似乎不再那麼神祕了。 本文希望能從巨集觀的角度帶剛接觸 Android 開發不久的同學認識一下 Gradle,如果對 Gradle 工作原理感興趣,希望能夠更加深入瞭解,建議閱讀 《Gradle In Action》 這本書。

背景

隨著專案規模增大,軟體工程師需要考慮的事情會越來越多。成功構建並執行一個專案不再像單檔案的 HelloWorld 一樣簡單。隨著持續整合思想的普及,一次成功的構建可能分為 checkStyle,Lint,編譯,單元測試,整合測試,程式碼裁剪,程式碼混淆,打包部署等多個步驟。如果專案中引用了第三方 lib,那麼第三方 lib 會有版本迭代,甚至多個第三方 lib 可能又依賴了不同版本的同一個第三方 lib,造成依賴版本衝突,事情會越來越複雜。我們需要使每一個 Commit 總是能構建出完全相同的結果,Git 對於二進位制檔案的版本管理又不是那麼得心應手,手動構建常常會引入人為變數導致構建出錯。所以構建過程自動化迫在眉睫。

常見的 Java 構建工具

  • Ant (Anothre Neat Tool) 2000年
    • 使用 XML 描述構建的步驟
    • 只負責構建步驟管理,如果要新增依賴管理的功能,還需要引入 Ivy
  • Maven 2004年
    • convention over configuration 的思想,無需配置或者僅需少量配置即可開始構建
    • 和 Ant 對比增加了依賴庫管理
  • Gradle 2007年
    • 使用 Groovy DSL 替代繁瑣的 XML
    • 支援增量構建
    • 專案結構更加靈活

Google 基於 Gradle 通過 Android Gradle Plugin 提供了自動化構建的工具,對開發者隱藏了大量的繁瑣的構建過程,暴露一些可被開發者配置的屬性,大大的簡化了 Android 專案管理的複雜度的同時又不失靈活性。

在這裡列舉的構建工具不止可以用來構建 Java 相關的專案。只要能表達出構建步驟,就可以使用這些工具來進行專案構建。比如,你可以使用 Gradle 來構建一個 iOS 的專案。

Gradle Wrapper:

The Wrapper is a script that invokes a declared version of Gradle, downloading it beforehand if necessary. As a result, developers can get up and running with a Gradle project quickly without having to follow manual installation processes saving your company time and money.

docs.gradle.org/current/use…

構建工具也是需要版本迭代的,一個大的版本迭代可能不會提供向前的相容性,也就是說,在 A 機器上和 B 機器上裝了兩個不同版本的 Gradle,結果可能導致同一個專案,在 A 的機器上可以成功構建,而在 B 的機器上會構建失敗。 為了避免這個問題,保證每個 Commit 總能構建出完全相同的結果。Gradle 提供了 Gradle Wrapper,通過 Wrapper 執行 Gradle Task 的時候,會先檢查 gradle-wrapper.properties 中指定的位置下,指定版本的 Gradle 是否安裝,如果已經安裝,則將該 Gradle Task 交給 Gradle 處理。如果沒有安裝,則先下載安裝指定版本的 Gradle,然後再將 Gradle Task 交給 Gradle 處理。 gradlew 是一個 script,是 Gradle Wrapper 的入口,Windows 下是 gradlew.bat。 gradle-wrapper.jar 提供了 Gradlew Wrapper 的核心功能。

目錄結構如下圖:

圖片

如下圖所示是一個典型的使用 Gradle 進行構建的 Android 工程。 工程中包含兩個 Project:

  1. TutorialAndroid -- RootProject
  2. app -- SubProject

圖片
可以使用如下命令檢視工程中的 Project

gradlew projects
複製程式碼

gradlew 是入口 Script, projects 實際上是 Gradle 一個內建的 Task。 關於 Task 的概念,下面再解釋。 執行上面的命令,結果如下圖所示,可以看到,一般我們開發時修改 **app **只是一個子專案,RootProject 實際上是 app 的上級目錄中的 TutorialAndroid。

圖片

構建過程

Gradle 的構建過程分為以下幾個階段: initialization -> configuration -> execution

  1. initialization phase
    • Gradle 使用 Project 物件來表示專案,在 initialization 階段,Gradle 會為每個參與本次構建的專案建立一個 Project 物件。
    • 因為 Gradle 支援多專案構建,所以在初始化階段的時候,需要判斷哪些專案需要參與本次構建。
    • Gradle 可以從 Project 的根目錄開始構建,也可以從任意包含 build file 的子檔案架開始構建。無論從哪裡開始構建,Gradle 都需要知道有哪些 Project 需要參與構建,Root Project 的 settings.gradle 中宣告瞭需要參與構建的 Project 的資訊。所以 Gradle 在這個階段做的事情,就是從當前目錄開始,逐級向上搜尋 settings.gradle ,如果找到了,就按照 settings.gradle 中宣告的資訊設定本次構建,如果最終沒有找到,那麼就預設只有當前所在的 Project 需要參與本次構建。
  2. configuration phase

    A Task represents a single atomic piece of work for a build, such as compiling classes or generating javadoc.

    A Task is made up of a sequence of Action objects. When the task is executed, each of the actions is executed in turn, by calling Action.execute(T). You can add actions to a task by calling Task.doFirst(org.gradle.api.Action) or Task.doLast(org.gradle.api.Action).

    docs.gradle.org/current/dsl…

    • Task 屬於 Project 物件。可以在 build.gradle 檔案中簡單定義 Task
     // 定義好 Task 之後,就可以通過 `gradlew simpleTask` 來執行指定的 Task
     task simpleTask {
         doLast {
         println "This is a simple task."
         }
     }
    複製程式碼
    • 專案構建過程分為很多步驟,在 Gradle 中用 Task 來表示這些步驟,Task 之間可能有依賴關係,例如:必須先執行完 compile Task,才能執行 unitTest Task。在 configuration 階段,Gradle 會分析 Task 之間的依賴關係,配置初始化階段建立的 Project 物件。

    Gradle determines the subset of the tasks, created and configured during the configuration phase, to be executed. The subset is determined by the task name arguments passed to the gradle command and the current directory.

    docs.gradle.org/current/use…

    • 當一個 Project 的 Task 越來越複雜,或者多個專案都需要共用同一個 Task 的時候,為了提高程式碼複用性,可以編寫 Plugin 將建立 Task 等邏輯封裝起來。

      圖片
      build.gradle 中,如圖所示就是在使用封裝好的 Plugin。

    • 提高了程式碼複用性的同時,還需要提供足夠的靈活性。Plugin 可以通過 Extension 暴露一些可配置的屬性。這裡先不講,超綱了。

  3. execution phase
    • 根據上一步計算出的任務執行順序去執行需要執行的 Tasks。

以上就是 Gradle 的工作過程。

Tricks

  1. 使用 Proxy

    在國內特殊的網路環境,可以通過設定 Proxy 或 Repo Mirror 的方式來提高下載依賴的 Library 的速度。

    阿里提供的映象 maven.aliyun.com/mvn/view Gradle 使用 Java 的 Networking Properties 讀取 Proxy 引數。可供設定的引數參考以下文件。

  2. 注意你的電腦中執行了多少 Gradle Daemon

    Gradle 提供了 Daemon 機制來提高構建速度,但是 Gradle Daemon 的複用是有條件的。 如果恰巧給 Gradle Daemon 設定了一個比較大的 maximum heap size, 可能在開發的過程中,多個 Daemon 會佔用過多的記憶體,影響電腦執行速度。除了前面給出的條件,還有兩點是之前開發過程中遇到過的:

    • 沒有正確使用 Gradle 提供的 wrapper Task 去升級 Gradle 版本,導致在使用 Gradle Wrapper Script 執行任務時,判斷 Gradle 版本的函式不相容,啟用多個 Daemon。這個問題的表現方式一般發現同一個版本的 Gradle 被 Gradle Wrapper 重複下載。
    • IDE 提供了 Build in JRE,導致在 IDE 中執行的 Gradle 和在 Terminal 下執行的 Gradle 雖然版本相同,但是無法複用。可以統一兩者使用的 JRE。比如在 Android Studio 中開啟 Project Structure,在 SDK Location Tab 下設定 JDK Location,使其和 Terminal 中使用的 Java 路徑統一。

參考資料

《Gradle In Action》 -- Benjamin Muschko

Gradle 最佳實踐

構建工具的進化:ant, maven, gradle

@Eric

相關文章