秒懂Gradle之從完全懵逼到是懂非懂

【版權申明】非商業目的註明出處可自由轉載
博文地址:
出自:shusheng007

首發於個人博客

概述

最近公司給Pluralsight 上開了賬號,然後評測了一下自己Android相關的知識,發現gradle方面的知識比較欠缺,仔細想想也確實是,自己從來沒有仔細研究過這個構建工具。剛好這兩天有點時間,查閱了一下官方文檔,爭取從概念上理解這個構建工具,做到可以寫build script,可以寫gradle plugin 。

相信有很多做了幾年Android開發的同學對於構建工具gradle仍然是一知半解的,甚至都不能完全理解那些build script,更別說修改了,每次要實現一個功能時都是Google一下,然後copy到自己的項目中,如果OK了就很開心,如果不OK就再搜一個,完全不能理解其中含義。這是最要命的,學習什麼東西都應該從概念和原理上理解它,特別是當這個東西你已經接觸很久了,更應該去理解其背後的設計。那樣不僅記得牢,還會舉一反三,進而在其基礎上創新。

本文確切的說是一篇讀書筆記,閱讀完本文後你會在腦中對gradle有個大概的理解,特別是使用過gradle的同學會有一種豁然開朗的感覺,你會突然發現你熟悉的那些build script 原來是這麼個意思啊。。。這種醍醐灌頂的感覺是最好的。俗話說,授人以魚不如授人以漁,在行文中我也會試圖告訴你在遇到問題的解決思路。

本文包括的內容

  • gradle 概述
  • gradle 腳本基礎
  • gradle 腳本實例

Gradle 概述

Gradle 是一個開源的自動構建工具,其幾乎可以構建任何類型的軟件,而不僅限於Java項目。

特點

Gradle官方宣稱其具有如下特點:

  1. 高效 (High performance)
  2. 基於JVM ,其運行在JVM上 (JVM foundation)
  3. 遵守慣例(Conventions),其在Ant和Maven之後出現,使用了很多約定俗成的概念
  4. 易於擴展 (Extensibility)
  5. IDE支持(IDE support),例如 Android Studio, IntelliJ IDEA, Eclipse和NetBeans
  6. 可視化(Insight),通過Build scans 可以生成構建信息,易於調試和分析

俯瞰Gradle:

會當凌絕頂,一覽衆山小,就如《金字塔原理》描述的那樣,看待一個事物應該先從全局着眼,然後再深入細節,才能做到心中有數。下面幾點是我們必須要知道的:

  1. Gradle是一個通用的自動化構建工具,而不針對某個特定平臺及語言
  2. 核心模式是基於Task的
    在這裏插入圖片描述
    如上圖所示,其會構建一張Task的依賴圖,整個構建過程就變成了一個task接着一個task的執行,直達完成的過程

task是Gradle的最小執行單元,由如下3部分構成:

  • 輸入:值,文件等
  • 動作:執行的動作
  • 輸出:值,文件等

前一個task的輸出是後一個task的輸入

  1. gradle分3個階段順序執行

    • 初始化階段: 設置構建環境
    • 配置階段 : 確定任務執行序列, 此階段每次 build run 都會執行
    • 執行階段: 按照配置執行
  2. . 支持多種擴展方式

    • 自定義Task類型
    • 自定義Task動作 Task.doFirst() and Task.doLast() methods.
    • 在project 與 task 使用 Extra properties
  3. 構建腳本基於API調用,而非基於配置
    你可以將構建腳本當代碼閱讀,而不是配置文件。它只會告訴你如何一步一步的將軟件構建出來,而不會告訴你每一步是如何做的。這也是與我們日常工作最密切的部分。

閱讀完以上的5點,你應該對gradle有一個模糊的概念了,接下來纔是本文的重點,也是與日常開發息息相關的:gradle script

Gradle 腳本

Gradle script 是用來指導Gradle 如何構建項目的,一般由Groovy DSL 編寫,現在也支持Kotlin DSL 編寫。

以前我天真的以爲我不會寫gradle 腳本是因爲我不懂Groovy語言,這下好了,gradle支持了Kotlin我應該會寫了。後來發現完全不是那回事,人家使用的是領域特定語言,英文爲domain-specific language (DSL) ,與我們平時使用的 general-purpose language (GPL) 完全不是一回事,關鍵是它還有自己的語法和API,所以使用Kotlin和Groovy基本一毛一樣。

讓我們來感受一下使用兩種語言定義一個gradle的task

Groovy:

task hello{
	doLast{ println "hello world!" }
}

Kotlin

task ("hello"){
	doLast{ println "hello world!" }
}

上面兩種寫法幾乎是相同的,所以別再騙自己了,讓我們去懂它吧。

腳本基礎對象

理解 gradle 最關鍵的是要理解 projecttask,這兩個概念是構成gradle script的基石。

  • project
    這個project是指gradle的project,並非我們項目。具體代表什麼和我們要構建的軟件類型相關,例如我們建立了一個多module的Android 應用,各個module就各是一個gradle project, 他們一起構成了一個gradle build。project比較抽象,需要慢慢理解。
  • task
    task 代表構建過程中的不可分割的執行任務。
    例如編譯一下classes,創建一個JAR,產生Javadoc 等等,都可以是一個task。每個project又包含一個或者多個task。

依照IT世界的慣例,先寫一個hello world出來

  1. 安裝gradle
    如果是window系統,下載zip包並解壓,然後配置環境變量即可
  2. 創建一個文本文件,命名爲build.gradle
  3. 在此文件中創建一個名爲hello的task
    task hello{
    	doLast{ println "hello world!" }
    }
    
  4. 執行task
    導航到build.gradle 文件目錄下,在命令行中執行
    gradle hello
    
  5. 查看輸出
    > Task :hello
    hello world!
    
    BUILD SUCCESSFUL in 1s
    1 actionable task: 1 executed
    

是不是很簡單啊

上面的腳本創建了一個叫hello的task,然後輸出一句hello world

其中task是gradle的Api, 其是Project 接口的一個方法,簽名如下

//Creates a {@link Task} with the given name and adds it to this project.
Task task(String name) throws InvalidUserDataException;

注意,那個project是gradle的一個接口,這個可厲害了,我們都是通過它來使用gradle的功能的。簽名如下:

//This interface is the main API you use to interact with Gradle from your build file. From a <code>Project</code>,
//you have programmatic access to all of Gradle's features.
public interface Project extends Comparable<Project>, ExtensionAware, PluginAware {
	...
}

doLast 是Task接口裏面的一個方法,在一個task體執行完其他任務後執行。

doLast 源碼:

    @Override
    public Task doLast(final Closure action) {
        hasCustomActions = true;
        if (action == null) {
            throw new InvalidUserDataException("Action must not be null!");
        }
        taskMutator.mutate("Task.doLast(Closure)", new Runnable() {
            @Override
            public void run() {
                getTaskActions().add(convertClosureToAction(action, "doLast {} action"));
            }
        });
        return this;
    }

看到這些源碼是不是感覺很親切啊,爲什麼到了gradle script中就看不懂了呢?嗯,那是因爲gradle 腳本語法與傳統的groovy,kotlin 語法不一樣,其DSL,有自己的語法。

接下來,我們就簡單的介紹一下gradle script,達到可以滿足日常開發需求,不至於一遇到gradle 腳本就兩眼一抹黑,修改全靠搜,不行重複搜。

腳本基礎語法

要想讀懂gradle script,首先就需要熟悉其語法構成元素:

語法元素

  1. Project : build script 的隱含對象

    通過它來使用gradle的功能,例如下面我們熟悉的塊都是project對象的方法,只不過滿足一定的方法簽名,可以寫成block的形式,這個我們後面會細說。

    buildscript{
    	...
    }
    configurations {
        ...
    }
    
  2. 屬性
    能使用=$ (模板符號)的都是屬性

    version = '1.0.1'
    myCopyTask.description = 'Copies some files'
    
    file("$buildDir/classes")
    println "Destination: ${myCopyTask.destinationDir}"
    

    上面的代碼中,version和buildDir都是project的屬性,description和destinationDir 是myCopyTask 這個Task的屬性

  3. 方法
    ()的都是方法,但是groovy 支持無()調用,所以無括號時就看後面有沒有=號,沒有就是方法。

    ext.resourceSpec = copySpec()   // `copySpec()` comes from `Project`	
    file('src/main/java')
    println 'Hello, World!'
    

其中println 也是方法,這裏的方法一般都是實例方法,就是說其是某個實例的方法。

  1. Block
    一種特殊的方法,最有用也最具有迷惑性,其表現如下

    <obj>.<name> {
         ...
    }
    
    <obj>.<name>(<arg>, <arg>) {
         ...
    }
    

只有符合如下簽名的方法纔可以使用block語法

  • 至少有一個方法入參
  • 方法的最後一個入參類型必須是groovy.lang.Closure 或者 org.gradle.api.Action.

例如下面這個輸入project的copy方法

copy {
    into "$buildDir/tmp"
    from 'custom-resources'
}

它爲什麼可以寫成block的形式呢,讓我們看一下它的簽名。它有兩個版本,一個的入參是groovy.lang.Closure 類型,一個對應的版本入參是org.gradle.api.Action。Closure 版本是爲了向前兼容的,新加的API都是使用Action版本。

//Closure 版本
 / * Copies the specified files.  The given closure is used to configure a {@link CopySpec}, which is then used to copy the files*/
WorkResult copy(Closure closure);

//Action版本
WorkResult copy(Action<? super CopySpec> action);

可以看到,其簽名是符合block語法要求的,我們可以發現 into 和 from也是兩個方法,但是這兩個方法是屬於哪個類型的呢?是project嗎?
通過查詢確認不是。其實這兩個方法是屬於 CopySpec 類型的。

對於如何得知 block裏面的方法屬於哪個類型,就需要分情況了。對於 Closure 版本需要看註釋,或者文檔。對於Action版本看泛型類型就行,gradle新增的API 都使用 Action的方式,即使是老的API也添加了對應的Action版本。

Task 依賴

task hello {
    doLast {
        println 'Hello world!'
    }
}
task intro {
    dependsOn hello
    doLast {
        println "I'm Gradle"
    }
}

通過dependsOn,intro task 依賴了hello,當我們執行into Task時,hello task也就被執行了。

Extra task properties

這個用的很多,Extra 就相當於一個Map,我們可以把值按key-value的形式放在裏面,用的時候通過key來取。

task myTask {
    ext.myProperty = "myValue"
}

task printTaskProperties {
    doLast {
        println myTask.myProperty
    }
}

上面代碼在myTask中給其ext 放入了 myProperty- myValue ,然後在其他task中就可以通過key 獲取到那個value了。 groovy代碼表現的不明顯,讓我們看下kotlin的。

tasks.register("myTask") {
    extra["myProperty"] = "myValue"
}

tasks.register("printTaskProperties") {
    doLast {
        println(tasks["myTask"].extra["myProperty"])
    }
}

是不是清晰多了,先找到myTask 然後從其extr中使用myProperty 做爲可以獲取其value。

使用方法和Ant

task loadfile2 {
    doLast {
        fileList('./antLoadfileResources').each { File file ->
            ant.loadfile(srcFile: file, property: file.name)
            println " *** $file.name ***"
            println "${ant.properties[file.name]}"
        }
    }
}

File[] fileList(String dir) {
    file(dir).listFiles({file -> file.isFile() } as FileFilter).sort()
}

和其他腳本語言一樣,我們可以把常用的功能分裝成method,然後在Task中調用,方法的定義就非常接近Java了。

上面的方法fileList()作用是將某個目錄下的文件過濾出來並按名稱排序。在loadfile2 task 中使用ant task 讀取文件內容並展示。

有的同學又懵了,日了個狗,Ant又是什麼東西? 爲什麼說基礎紮實,經驗豐富的程序員學啥都快呢?因爲一個新的東西不會憑空出現,往往與既有的體系有千絲萬縷的聯繫,人類之所以可以不斷進步,就是因爲我們的知識可以一代一代的傳承。 簡單來說,Ant 與gradle是競品,Ant是gradle的前輩,gradle和maven是站在Ant的肩膀上發展起來的。所以gradle也會吸收Ant很多優秀的東西,包括思想和實現。

在build script 中添加外部依賴

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath group: 'commons-codec', name: 'commons-codec', version: '1.2'
    }
}

這個在android開發中已經見的很多了,不再贅述。

總結

行文到此就要結束了,如果你認真閱讀了以上內容,相信你對gradle script會有新的認識,特別是block語法,當你閱讀gradle 腳本的時候心中再也不慌了,雖然你可能不知道具體的含義,但是你清楚的知道他們背後是什麼,如何去查詢,這就是本文的目的。

想要更進一步,當然是去看官方網站 官網

如果有什麼問題可以在留言區留言討論。由於本人一向思維縝密,bug極少… Hi,施主你別噴我啊!施主,你別拍。。。施主。。。 我r, gsn。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章