【Android 修煉手冊】Gradle 篇 -- Gradle 的基本使用

ZY5A59發表於2019-05-09

預備知識

  1. 基本的 android 開發知識
  2. 瞭解 Android Studio 基本使用

看完本文可以達到什麼程度

  1. 掌握 gradle 的基本使用
  2. 瞭解 gradle 及 android gradle plugin
  3. 瞭解 gradle 構建階段及生命週期回撥
  4. 掌握 Task,Transform 等概念
  5. 學會自定義 task,自定義 gradle 外掛

如果您已經達到上面的程度,那麼可以不用再看下文了,直接看最後的總結即可

本文將從下面幾個部分進行講解:

gradle-summary

一、gradle 是什麼

gradle1
官方解釋是: Gradle is an open-source build automation tool focused on flexibility and performance. Gradle build scripts are written using a Groovy or Kotlin DSL.
可以從三個角度來理解

1. gradle 是一個自動化構建工具
gradle 是通過組織一系列 task 來最終完成自動化構建的,所以 task 是 gradle 裡最重要的概念
我們以生成一個可用的 apk 為例,整個過程要經過 資源的處理,javac 編譯,dex 打包,apk 打包,簽名等等步驟,每個步驟就對應到 gradle 裡的一個 task

gradle 可以類比做一條流水線,task 可以比作流水線上的機器人,每個機器人負責不同的事情,最終生成完整的構建產物

gradle-pipelining

2. gradle 指令碼使用了 groovy 或者 kotlin DSL
gradle 使用 groovy 或者 kotlin 編寫,不過目前還是 groovy 居多
那什麼是 DSL 呢?DSL 也就是 Domain Specific Language 的簡稱,是為了解決某一類任務專門設計的計算機語言
DSL 相對應的是 GPL (General-Purpose Language),比如 java
與 GPL 相比起來,DSL 使用簡單,定義比較簡潔,比起配置檔案,DSL 又可以實現語言邏輯
對 gradle 指令碼來說,他實現了簡潔的定義,又有充分的語言邏輯,以 android {} 為例,這本身是一個函式呼叫,引數是一個閉包,但是這種定義方式明顯要簡潔很多

3. gradle 基於 groovy 編寫,而 groovy 是基於 jvm 語言
gradle 使用 groovy 編寫,groovy 是基於 jvm 的語言,所以本質上是物件導向的語言,面嚮物件語言的特點就是一切皆物件,所以,在 gradle 裡,.gradle 指令碼的本質就是類的定義,一些配置項的本質都是方法呼叫,引數是後面的 {} 閉包
比如 build.gradle 對應 Project 類,buildScript 對應 Project.buildScript 方法

二、gradle 專案分析

gradle2

關於 gradle 的專案層次,我們新建一個專案看一下,專案地址在 EasyGradle

gradle-projcet

2.1 settings.gradle

settings.gradle 是負責配置專案的指令碼
對應 Settings 類,gradle 構建過程中,會根據 settings.gradle 生成 Settings 的物件
對應的可呼叫的方法在文件裡可以查詢
其中幾個主要的方法有:

  • include(projectPaths)
  • includeFlat(projectNames)
  • project(projectDir)

一般在專案裡見到的引用子模組的方法,就是使用 include,這樣引用,子模組位於根專案的下一級

include ':app'
複製程式碼

如果想指定子模組的位置,可以使用 project 方法獲取 Project 物件,設定其 projectDir 引數

include ':app'
project(':app').projectDir = new File('./app')
複製程式碼

2.2 rootproject/build.gradle

build.gradle 負責整體專案的一些配置,對應的是 Project
gradle 構建的時候,會根據 build.gradle 生成 Project 物件,所以在 build.gradle 裡寫的 dsl,其實都是 Project 介面的一些方法,Project 其實是一個介面,真正的實現類是 DefaultProject
build.gradle 裡可以呼叫的方法在 Project 可以查到
其中幾個主要方法有:

  • buildscript // 配置指令碼的 classpath
  • allprojects // 配置專案及其子專案
  • respositories // 配置倉庫地址,後面的依賴都會去這裡配置的地址查詢
  • dependencies // 配置專案的依賴

以 EasyGradle 專案來看

buildscript { // 配置專案的 classpath
    repositories {  // 專案的倉庫地址,會按順序依次查詢
        google()
        jcenter()
        mavenLocal()
    }
    dependencies { // 專案的依賴
        classpath 'com.android.tools.build:gradle:3.0.1'
        classpath 'com.zy.plugin:myplugin:0.0.1'
    }
}

allprojects { // 子專案的配置
    repositories {
        google()
        jcenter()
        mavenLocal()
    }
}
複製程式碼

2.3 module/build.gradle

build.gradle 是子專案的配置,對應的也是 Project 類
子專案和根專案的配置是差不多的,不過在子專案裡可以看到有一個明顯的區別,就是引用了一個外掛 apply plugin "com.android.application",後面的 android dsl 就是 application 外掛的 extension,關於 android plugin dsl 可以看 android-gradle-dsl
其中幾個主要方法有:

  • compileSdkVersion // 指定編譯需要的 sdk 版本
  • defaultConfig // 指定預設的屬性,會運用到所有的 variants 上
  • buildTypes // 一些編譯屬性可以在這裡配置,可配置的所有屬性在 這裡
  • productFlavor // 配置專案的 flavor

以 app 模組的 build.gradle 來看

apply plugin: 'com.android.application' // 引入 android gradle 外掛

android { // 配置 android gradle plugin 需要的內容
    compileSdkVersion 26
    defaultConfig { // 版本,applicationId 等配置
        applicationId "com.zy.easygradle"
        minSdkVersion 19
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
    }
    buildTypes { 
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions { // 指定 java 版本
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }

    // flavor 相關配置
    flavorDimensions "size", "color"
    productFlavors {
        big {
            dimension "size"
        }
        small {
            dimension "size"
        }
        blue {
            dimension "color"
        }
        red {
            dimension "color"
        }
    }
}

// 專案需要的依賴
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar']) // jar 包依賴
    implementation 'com.android.support:appcompat-v7:26.1.0' // 遠端倉庫依賴
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation project(':module1') // 專案依賴
}
複製程式碼

2.4 依賴

在 gradle 3.4 裡引入了新的依賴配置,如下:

新配置 棄用配置 行為 作用
implementation compile 依賴項在編譯時對模組可用,並且僅在執行時對模組的消費者可用。 對於大型多專案構建,使用 implementation 而不是 api/compile 可以顯著縮短構建時間,因為它可以減少構建系統需要重新編譯的專案量。 大多數應用和測試模組都應使用此配置。 api 只會暴露給直接依賴的模組,使用此配置,在模組修改以後,只會重新編譯直接依賴的模組,間接依賴的模組不需要改動
api compile 依賴項在編譯時對模組可用,並且在編譯時和執行時還對模組的消費者可用。 此配置的行為類似於 compile(現在已棄用),一般情況下,您應當僅在庫模組中使用它。 應用模組應使用 implementation,除非您想要將其 API 公開給單獨的測試模組。 api 會暴露給間接依賴的模組,使用此配置,在模組修改以後,模組的直接依賴和間接依賴的模組都需要重新編譯
compileOnly provided 依賴項僅在編譯時對模組可用,並且在編譯或執行時對其消費者不可用。 此配置的行為類似於 provided(現在已棄用)。 只在編譯期間依賴模組,打包以後執行時不會依賴,可以用來解決一些庫衝突的問題
runtimeOnly apk 依賴項僅在執行時對模組及其消費者可用。 此配置的行為類似於 apk(現在已棄用)。 只在執行時依賴模組,編譯時不依賴

還是以 EasyGradle 為例,看一下各個依賴的不同: 專案裡有三個模組:app,module1, module2
模組 app 中有一個類 ModuleApi
模組 module1 中有一個類 Module1Api
模組 module2 中有一個類 Module2Api
其依賴關係如下:

dep

implementation 依賴
當 module1 使用 implementation 依賴 module2 時,在 app 模組中無法引用到 Module2Api 類

implementation

api 依賴
當 module1 使用 api 依賴 module2 時,在 app 模組中可以正常引用到 Module2Api 類,如下圖

api

compileOnly 依賴
當 module1 使用 compileOnly 依賴 module2 時,在編譯階段 app 模組無法引用到 Module2Api 類,module1 中正常引用,但是在執行時會報錯

compileOnly

反編譯打包好的 apk,可以看到 Module2Api 是沒有被打包到 apk 裡的

ompileOnly-apk

runtimeOnly 依賴
當 module1 使用 runtimeOnly 依賴 module2 時,在編譯階段,module1 也無法引用到 Module2Api

runtimeOnly

2.5 flavor

在介紹下面的流程之前,先明確幾個概念,flavor,dimension,variant
在 android gradle plugin 3.x 之後,每個 flavor 必須對應一個 dimension,可以理解為 flavor 的分組,然後不同 dimension 裡的 flavor 兩兩組合形成一個 variant
舉個例子 如下配置:

flavorDimensions "size", "color"

productFlavors {
    big {
        dimension "size"
    }
    small {
        dimension "size"
    }
    blue {
        dimension "color"
    }
    red {
        dimension "color"
    }
}
複製程式碼

那麼生成的 variant 對應的就是 bigBlue,bigRed,smallBlue,smallRed
每個 variant 可以對應的使用 variantImplementation 來引入特定的依賴,比如:bigBlueImplementation,只有在 編譯 bigBlue variant的時候才會引入

三、gradle wrapper

gradle3

gradlew / gradlew.bat 這個檔案用來下載特定版本的 gradle 然後執行的,就不需要開發者在本地再安裝 gradle 了。這樣做有什麼好處呢?開發者在本地安裝 gradle,會碰到的問題是不同專案使用不同版本的 gradle 怎麼處理,用 wrapper 就很好的解決了這個問題,可以在不同專案裡使用不同的 gradle 版本。gradle wrapper 一般下載在 GRADLE_CACHE/wrapper/dists 目錄下

gradle/wrapper/gradle-wrapper.properties 是一些 gradlewrapper 的配置,其中用的比較多的就是 distributionUrl,可以執行 gradle 的下載地址和版本
gradle/wrapper/gradle-wrapper.jar 是 gradlewrapper 執行需要的依賴包

四、gradle init.gradle

gradle4

在 gradle 裡,有一種 init.gradle 比較特殊,這種指令碼會在每個專案 build 之前先被呼叫,可以在其中做一些整體的初始化操作,比如配置 log 輸出等等
使用 init.gradle 的方法:

  1. 通過 --init-script 指定 init.gradle 位置 eg: gradlew --init-script initdir/init.gradle
  2. init.gradle 檔案放在 USER_HOME/.gradle/ 目錄下
  3. .gradle 指令碼放在 USER_HOME/.gradle/init.d/ 目錄下
  4. .gradle 指令碼放在 GRDALE_HOME/init.d/ 目錄下

五、gradle 生命週期及回撥

gradle5

gradle 構建分為三個階段
初始化階段
初始化階段主要做的事情是有哪些專案需要被構建,然後為對應的專案建立 Project 物件

配置階段
配置階段主要做的事情是對上一步建立的專案進行配置,這時候會執行 build.gradle 指令碼,並且會生成要執行的 task

執行階段
執行階段主要做的事情就是執行 task,進行主要的構建工作

gradle 在構建過程中,會提供一些列回撥介面,方便在不同的階段做一些事情,主要的介面有下面幾個

gradle.addBuildListener(new BuildListener() {
    @Override
    void buildStarted(Gradle gradle) {
        println('構建開始')
        // 這個回撥一般不會呼叫,因為我們註冊的時機太晚,註冊的時候構建已經開始了,是 gradle 內部使用的
    }

    @Override
    void settingsEvaluated(Settings settings) {
        println('settings 檔案解析完成')
    }

    @Override
    void projectsLoaded(Gradle gradle) {
        println('專案載入完成')
        gradle.rootProject.subprojects.each { pro ->
            pro.beforeEvaluate {
                println("${pro.name} 專案配置之前呼叫")
            }
            pro.afterEvaluate{
                println("${pro.name} 專案配置之後呼叫")
            }
        }
    }

    @Override
    void projectsEvaluated(Gradle gradle) {
        println('專案解析完成')
    }

    @Override
    void buildFinished(BuildResult result) {
        println('構建完成')
    }
})

gradle.taskGraph.whenReady {
    println("task 圖構建完成")
}
gradle.taskGraph.beforeTask {
    println("每個 task 執行前會調這個介面")
}
gradle.taskGraph.afterTask {
    println("每個 task 執行完成會調這個介面")
}
複製程式碼

六、自定義 task

gradle6

預設建立的 task 繼承自 DefaultTask 如何宣告一個 task

task myTask {
    println 'myTask in configuration'
    doLast {
        println 'myTask in run'
    }
}

class MyTask extends DefaultTask {
    @Input Boolean myInputs
    @Output 
    @TaskAction
    void start() {
    }
}

tasks.create("mytask").doLast {
}
複製程式碼

Task 的一些重要方法分類如下:

  • Task 行為
    Task.doFirst
    Task.doLast

  • Task 依賴順序
    Task.dependsOn
    Task.mustRunAfter
    Task.shouldRunAfter
    Task.finalizedBy

  • Task 的分組描述
    Task.group
    Task.description

  • Task 是否可用
    Task.enabled

  • Task 輸入輸出
    gradle 會比較 task 的 inputs 和 outputs 來決定 task 是否是最新的,如果 inputs 和 outputs 沒有變化,則認為 task 是最新的,task 就會跳過不執行
    Task.inputs
    Task.outputs

  • Task 是否執行
    可以通過指定 Task.upToDateWhen = false 來強制 task 執行 Task.upToDateWhen

比如要指定 Task 之間的依賴順序,寫法如下:

task task1 {
    doLast {
        println('task2')
    }
}
task task2 {
    doLast {
        println('task2')
    }
}
task1.finalizedBy(task2)
task1.dependsOn(task2)
task1.mustRunAfter(task2)
task1.shouldRunAfter(task2)
task1.finalizedBy(task2)
複製程式碼

七、Android transform

gradle7

android gradle plugin 提供了 transform api 用來在 .class to dex 過程中對 class 進行處理,可以理解為一種特殊的 Task,因為 transform 最終也會轉化為 Task 去執行
要實現 transform 需要繼承 com.android.build.api.transform.Transform 並實現其方法,實現了 Transform 以後,要想應用,就呼叫 project.android.registerTransform()

public class MyTransform extends Transform {
    @Override
    public String getName() {
        // 返回 transform 的名稱,最終的名稱會是 transformClassesWithMyTransformForDebug 這種形式   
        return "MyTransform";
    }

    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        /**
        返回需要處理的資料型別 有 下面幾種型別可選
        public static final Set<ContentType> CONTENT_CLASS = ImmutableSet.of(CLASSES);
        public static final Set<ContentType> CONTENT_JARS = ImmutableSet.of(CLASSES, RESOURCES);
        public static final Set<ContentType> CONTENT_RESOURCES = ImmutableSet.of(RESOURCES);
        public static final Set<ContentType> CONTENT_NATIVE_LIBS = ImmutableSet.of(NATIVE_LIBS);
        public static final Set<ContentType> CONTENT_DEX = ImmutableSet.of(ExtendedContentType.DEX);
        public static final Set<ContentType> DATA_BINDING_ARTIFACT = ImmutableSet.of(ExtendedContentType.DATA_BINDING);
        */
        return TransformManager.CONTENT_CLASS;
    }

    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        /**
        返回需要處理內容的範圍,有下面幾種型別
        PROJECT(1), 只處理專案的內容
        SUB_PROJECTS(4), 只處理子專案
        EXTERNAL_LIBRARIES(16), 只處理外部庫
        TESTED_CODE(32), 只處理當前 variant 對應的測試程式碼
        PROVIDED_ONLY(64), 處理依賴
        @Deprecated
        PROJECT_LOCAL_DEPS(2),
        @Deprecated
        SUB_PROJECTS_LOCAL_DEPS(8);
        */
        return Sets.immutableEnumSet(QualifiedContent.Scope.PROJECT);
    }

    @Override
    public boolean isIncremental() {
        // 是否增量,如果返回 true,TransformInput 會包括一份修改的檔案列表,返回 false,會進行全量編譯,刪除上一次的輸出內容
        return false;
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        // 在這裡處理 class
        super.transform(transformInvocation)
        // 在 transform 裡,如果沒有任何修改,也要把 input 的內容輸出到 output,否則會報錯
        for (TransformInput input : transformInvocation.inputs) {
            input.directoryInputs.each { dir ->
                // 獲取對應的輸出目錄
                File output = transformInvocation.outputProvider.getContentLocation(dir.name, dir.contentTypes, dir.scopes, Format.DIRECTORY)
                dir.changedFiles // 增量模式下修改的檔案
                dir.file // 獲取輸入的目錄
                FileUtils.copyDirectory(dir.file, output) // input 內容輸出到 output
            }
            input.jarInputs.each { jar ->
                // 獲取對應的輸出 jar
                File output = transformInvocation.outputProvider.getContentLocation(jar.name, jar.contentTypes, jar.scopes, Format.JAR)
                jar.file // 獲取輸入的 jar 檔案
                FileUtils.copyFile(jar.file, output) // input 內容輸出到 output
            }
        }
    }
}

// 註冊 transform
android.registerTransform(new MyTransform())
複製程式碼

在 transform 中的處理,一般會涉及到 class 檔案的修改,操縱位元組碼的工具一般是 javasist 和 asm 居多,這兩個工具在這裡先不介紹了。後面有機會會展開說一下

八、自己寫 plugin

gradle8

gradle 的外掛可以看作是一系列 task 的集合
在 android 工程的 build.gradle 指令碼里,第一行就是 apply plugin: 'com.android.application',這個就是引入 android gradle 外掛,外掛裡有 android 打包相關的 task
關於 android gradle plugin 的原始碼分析,在後面會講到,現在先看看如何實現一個自己的 plugin

8.1 初始化工程

  1. 在 android studio 中建立一個 java module
  2. 在 src/main 目錄下建立 groovy 目錄,然後建立自己的包名和外掛類
  3. 在 src/main 目錄下建立 resources/META-INFO/gradle-plugins 目錄,建立 ,myplugin.properties 檔案,檔案裡內容是
implementation-class=com.zy.plugin.MyPlugin // 這裡是自己的外掛類
複製程式碼
  1. 修改 build.gradle 檔案
// 引入 groovy 和 java 外掛
apply plugin: 'groovy'
apply plugin: 'java'

buildscript {
    repositories {
        mavenLocal()
        maven { url 'http://depot.sankuai.com/nexus/content/groups/public/' }
        maven { url 'https://maven.google.com' }
        jcenter()
    }
}

repositories {
    mavenLocal()
    maven {
        url "http://mvn.dianpingoa.com/android-nova"
    }
    maven {
        url 'http://depot.sankuai.com/nexus/content/groups/public/'
    }
    maven { url 'https://maven.google.com' }
}

dependencies {
    compile gradleApi()
    compile localGroovy()
    compile 'com.android.tools.build:gradle:3.0.1'
}


複製程式碼

現在為止,專案結構是這個樣子的

gradle-plugins

8.2 建立 Plugin

在剛才建立的外掛類裡,就可以寫外掛的程式碼了。外掛類繼承 Plugin,並實現 apply 介面,apply 就是在 build.gradle 裡 apply plugin 'xxx' 的時候要呼叫的介面了
外掛開發可以使用 groovy 和 java,使用 groovy 的話可以有更多的語法糖,開發起來更方便一些

package com.zy.plugin

import org.gradle.api.Plugin
import org.gradle.api.Project

class MyPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        println("apply my plugin")
    }
}
複製程式碼

8.3 建立外掛的 task

我們再定義一個 task 類 MyTask,繼承自 DefaultTask,簡單的輸出一些資訊

package com.zy.plugin

import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction

class MyTask extends DefaultTask {

    @TaskAction
    void action() {
        println('my task run')
    }
}
複製程式碼

然後在 plugin 中註冊這個 task

class MyPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        println("apply my plugin")
        project.tasks.create("mytask", MyTask.class)
    }
}
複製程式碼

8.4 本地安裝外掛

這樣一個簡單的外掛就開發好了,如何使用呢
我們首先需要在 build.gradle 中引入 maven 外掛,並且配置 install 相關的屬性

apply plugin: 'maven'

install {
    repositories.mavenInstaller {
        pom.version = '0.0.1' // 配置外掛版本號
        pom.artifactId = 'myplugin' // 配置外掛標識
        pom.groupId = 'com.zy.plugin' // 配置外掛組織
    }
}
複製程式碼

之後執行 ./gradlew install 便會把外掛安裝在本地 maven 倉庫
之後在使用的地方引入我們外掛的 classpath

classpath 'com.zy.plugin:myplugin:0.0.1'
複製程式碼

之後載入外掛

apply plugin; 'myplugin' // 這裡的 myplugin 是前面說的 myplugin.properties 的名字
複製程式碼

然後執行 ./gradlew tasks --all | grep mytask,就可以看到我們在 plugin 裡新增的 task 了
./gradlew mytasks 就可以執行 task 了

8.5 打包釋出

在外掛 build.gradle 裡新增上傳的配置如下

uploadArchives {
    repositories {
        mavenDeployer {
            repository(url: "mavenUrl")
            pom.version = '0.0.1'
            pom.artifactId = 'myplugin'
        }
    }
}
複製程式碼

執行 ./gradlew uploadArchives 就可以了

8.6 除錯外掛

那麼開發外掛的時候如何除錯呢?
1.首先在 as 中新增一個 remote 配置

debug1
debug2

2.之後在執行 task 的時候增加下面的引數

./gradlew app:mytask -Dorg.gradle.debug=true
複製程式碼

此時可以看到 gradle 在等待 debug 程式連線

debug3

3.之後在外掛程式碼中打好斷點,在 as 中點選 debug 按鈕,就可以除錯外掛程式碼了

debug4

九、重點總結

主要要點如下圖:

gradle8

其中一定要掌握的如下:

  1. gradle dsl 查詢地址 docs.gradle.org/current/dsl…
  2. android gradle plugin dsl 查詢地址 google.github.io/android-gra…
  3. gradle 構建生命週期和回撥
  4. implementation / api
  5. flavor
  6. 自定義 Task
  7. 自定義 Transform 和 自定義外掛可以作為擴充套件內容

【Android 修煉手冊】系列內容 每週五更新
歡迎關注下面賬號,獲取更新:
微信搜尋公眾號: ZYLAB
Github
掘金

相關文章