请点赞加关注,你的反对对我十分重要,满足下我的虚荣心。

Hi,我是小彭。本文已收录到 GitHub · Android-NoteBook 中。这里有 Android 进阶成长常识体系,有气味相投的敌人,欢送跟着我一起成长。(联系方式在 GitHub)

前言

Gradle 实质上是高度模块化的构建逻辑,便于重用并与别人分享。例如,咱们相熟的 Android 构建流程就是由 Android Gradle Plugin 引入的构建逻辑。在这篇文章里,我将带你探讨 Gradle 插件的应用办法、开发步骤和技巧总结。

这篇文章是全面把握 Gradle 构建零碎系列的第 2 篇:

  • 1、Gradle 根底
  • 2、Gradle 插件
  • 3、Gradle 依赖治理
  • 4、APG Transform

1. 意识 Gradle 插件

1.1 什么是 Gradle 插件

Gradle 和 Gradle 插件是两个齐全不同的概念,Gradle 提供的是一套外围的构建机制,而 Gradle 插件则是运行在这套机制上的一些具体构建逻辑,实质上和 .gradle 文件是雷同。例如,咱们相熟的编译 Java 代码的能力,都是由插件提供的。

1.2 Gradle 插件的长处

尽管 Gradle 插件与 .gradle 文件实质上没有区别,.gradle 文件也能实现 Gradle 插件相似的性能。然而,Gradle 插件应用了独立模块封装构建逻辑,无论是从开发开始应用来看,Gradle 插件的整体体验都更敌对。

  • 逻辑复用: 将雷同的逻辑提供给多个类似我的项目复用,缩小反复保护相似逻辑开销。当然 .gradle 文件也能做到逻辑复用,但 Gradle 插件的封装性更好;
  • 组件公布: 能够将插件公布到 Maven 仓库进行治理,其余我的项目能够应用插件 ID 依赖。当然 .gradle 文件也能够放到一个近程门路被其余我的项目援用;
  • 构建配置: Gradle 插件能够申明插件扩大来裸露可配置的属性,提供定制化能力。当然 .gradle 文件也能够做到,但实现会麻烦些。

1.3 插件的两种实现模式

Gradle 插件的外围类是 Plugin<T>,个别应用 Project 作为泛型实参。当应用方引入插件后,其实就是调用了 Plugin#apply() 办法,咱们能够把 apply() 办法了解为插件的执行入口。例如:

MyCustomGradlePlugin.groovy

public class MyCustomGradlePlugin implements Plugin<Project> {    @Override    void apply(Project project) {        println "Hello."    }}

如果依据实现模式分类(MyCustomGradlePlugin 的代码地位),能够把 Gradle 插件分为 2 类:

  • 1、脚本插件: 脚本插件就是一个一般的脚本文件,它能够被导入都其余构建脚本中。有的敌人说脚本插件也须要应用 Plugin 接口才算脚本插件,例如:

build.gradle

apply plugin: MyCustomGradlePluginclass MyCustomGradlePlugin implements Plugin<Project> {    ...}
  • 2、二进制插件 / 对象插件: 在一个独自的插件模块中定义,其余模块通过 Plugin ID 利用插件。因为这种形式公布和复用更加敌对,咱们个别接触到的 Gradle 插件都是指二进制插件的模式。

1.4 利用插件的步骤

咱们总结下应用二进制插件的步骤:

  • 1、将插件增加到 classpath: 将插件增加到构建脚本的 classpath 中,咱们的 Gradle 构建脚本能力利用插件。这里辨别本地依赖和近程依赖两种状况。

本地依赖: 指间接依赖本地插件源码,个别在调试插件的阶段是应用本地依赖的形式。例如:

我的项目 build.gradle

buildscript {    ...    dependencies {        // For Debug        classpath project(":easyupload")    }}

近程依赖: 指依赖已公布到 Maven 仓库的插件,个别咱们都是用这种形式依赖官网或第三方实现的 Gradle 插件。例如:

我的项目 build.gradle

buildscript {    repositories {        google()        jcenter()    }    dependencies {        classpath 'com.android.tools.build:gradle:3.5.3'        // 也能够应用另一种等价语法:        classpath group: 'com.android.tools.build ', name: 'gradle ', version: '3.5.3'    }    ...}
  • 2、应用 apply 利用插件: 在须要应用插件的 .gradle 脚本中应用 apply 利用插件,这将创立一个新的 Plugin 实例,并执行 Plugin#apply() 办法。例如:
apply plugin: 'com.android.application'// 或者plugins {    // id «plugin id» [version «plugin version»] [apply «false»]    id 'com.android.application'}
留神: 不反对在一个 build.gradle 中同时应用这两种语法。

1.5 非凡的 buildSrc 模块

插件模块的名称是任意的,除非应用了一个非凡的名称 “buildSrc”,buildSrc 模块是 Gradle 默认的插件模块。buildSrc 模块实质上和一般的插件模块是一样的,有一些小区别:

  • 1、buildSrc 模块会被自动识别为参加构建的模块,因而不须要在 settings.gradle 中应用 include 引入,就算引入了也会编译出错:
Build OutPut:'buildSrc' cannot be used as a project name as it is a reserved name
  • 2、buildSrc 模块会主动被增加到构建脚本的 classpath 中,不须要手动增加:
buildscript {    ...    dependencies {        // 不须要手动增加        // classpath project(":buildSrc")    }}
  • 3、buildSrc 模块的 build.gradle 执行机会早于其余 Project:
Executing tasks: [test] settings.gradle:This is executed during the initialization phase.> Configure project :buildSrcbuild.gradle:buildSrc.> Task :buildSrc:compileJava NO-SOURCE> Task :buildSrc:compileGroovy NO-SOURCE> Task :buildSrc:pluginDescriptors UP-TO-DATE> Task :buildSrc:processResources NO-SOURCE> Task :buildSrc:classes UP-TO-DATE> Task :buildSrc:jar UP-TO-DATE> Task :buildSrc:assemble UP-TO-DATE> Task :buildSrc:pluginUnderTestMetadata UP-TO-DATE> Task :buildSrc:compileTestJava NO-SOURCE> Task :buildSrc:compileTestGroovy NO-SOURCE> Task :buildSrc:processTestResources NO-SOURCE> Task :buildSrc:testClasses UP-TO-DATE> Task :buildSrc:test NO-SOURCE> Task :buildSrc:validatePlugins UP-TO-DATE> Task :buildSrc:check UP-TO-DATE> Task :buildSrc:build UP-TO-DATE...> Configure project :...> Task :test...BUILD SUCCESSFUL in 19s


2. 自定义 Gradle 插件的步骤

这一节咱们来讲实现 Gradle 插件的具体步骤,根本步骤分为 5 步:

  • 1、初始化插件目录构造
  • 2、创立插件实现类
  • 3、配置插件实现类
  • 4、公布插件
  • 5、应用插件

2.1 初始化插件目录构造

首先,咱们在 Android Studio 新建一个 Java or Kotlin Library 模块,这里以非 buildSrc 模块的状况为例:

而后,将模块 build.gradle 文件替换为以下内容:

模块 build.gradle

plugins {    id 'groovy' // Groovy Language        id 'org.jetbrains.kotlin.jvm' // Kotlin     id 'java-gradle-plugin' // Java Gradle Plugin}
  • groovy 插件: 应用 Groovy 语言开发必备;
  • org.jetbrains.kotlin.jvm 插件: 应用 Kotlin 语言开发必备;
  • java-gradle-plugin 插件: 用于帮忙开发 Gradle 插件,会主动利用 Java Library 插件,并在 dependencies 中增加 implementation gradleApi()

最初,依据你须要的开发语言补充对应的源码文件夹,不同语言有默认的源码文件夹,你也能够在 build.gradle 文件中从新指定:

模块 build.gradle

plugins {    id 'groovy' // Groovy Language    id 'org.jetbrains.kotlin.jvm' // Kotlin     id 'java-gradle-plugin' // Java Gradle Plugin}sourceSets {    main {        groovy {            srcDir 'src/main/groovy'        }        java {            srcDir 'src/main/java'        }        resources {            srcDir 'src/main/resources'        }    }}

插件目录构造:

2.2 创立插件实现类

新建一个 Plugin<T> 实现类,并重写 apply 办法中增加构建逻辑,例如:

com.pengxr.easyupload.EasyUpload.groovy

class EasyUpload implements Plugin<Project> {    @Override    void apply(Project project) {        // 构建逻辑        println "Hello."    }}

2.3 配置插件实现类

在模块 build.gradle 文件中减少以下配置,gradlePlugin 定义了插件 ID 和插件实现类的映射关系:

gradlePlugin {    plugins {        modularPlugin {            // Plugin id.            id = 'com.pengxr.easyupload'            // Plugin implementation.            implementationClass = 'com.pengxr.easyupload.EasyUpload'        }    }}

这其实是 Java Gradle Plugin 提供的一个简化 API,其背地会主动帮咱们创立一个 [插件ID].properties 配置文件,Gradle 就是通过这个文件类进行匹配的。如果你不应用 gradlePlugin API,间接手动创立 [插件ID].properties 文件,作用是齐全一样的。

要点:

  • 1、[插件ID].properties 文件名是插件 ID,用于利用插件
  • 2、[插件ID].properties 文件内容配置了插件实现类的映射,须要应用implementation-class来指定插件实习类的全限定类名
implementation-class=com.pengxr.easyupload.EasyUpload

2.4 公布插件

咱们应用 maven 插件 来公布仓库,在模块 build.gradle 文件中减少配置:

模块 build.gradle

plugins {    id 'groovy' // Groovy Language        id 'org.jetbrains.kotlin.jvm' // Kotlin     id 'java-gradle-plugin' // Java Gradle Plugin}gradlePlugin {    plugins {        modularPlugin {            // Plugin id.            id = 'com.pengxr.easyupload'            // Plugin implementation.            implementationClass = 'com.pengxr.easyupload.EasyUpload'        }    }}uploadArchives {    repositories {        mavenDeployer {            repository(url: uri('../localMavenRepository/snapshot'))            pom.groupId = 'com.pengxr'            pom.artifactId = 'easyupload'            pom.version = '1.0.0'        }    }}

执行 uploadArchives 工作,会公布插件到我的项目根目录中的 localMavenRepository 文件夹,理论我的项目中通常是公布到 Nexus 私库或 Github 公共库等。不相熟组件公布的话能够回顾:Android工程化实际:组件化公布,此处不开展。

2.5 应用插件

在我的项目级 build.gradle 文件中将插件增加到 classpath:

我的项目 build.gradle

buildscript {    repositories {        google()        jcenter()        maven { url "$rootDir/localMavenRepository/snapshot" }        maven { url "$rootDir/localMavenRepository/release" }    }    dependencies {        // For debug        // classpath project(":easyupload")        classpath "com.pengxr:easyupload:1.0.0"    }    ...}

在模块级 build.gradle 文件中 apply 插件:

模块 build.gradle

// '我的项目 build.gradle' 是在 gradlePlugin 中定义的插件 IDapply plugin: 'com.pengxr.easyupload'

实现以上步骤并同步我的项目,从 Build Output 能够看到咱们的插件失效了:

Build Output:Hello.

到这里,自定义 Gradle 插件最根本的步骤就实现了,接下来就能够在 Plugin#apply 办法中开始你的表演。


3. 插件扩大机制

Extension 扩大是插件为内部构建脚本提供的配置项,用于反对内部自定义插件的工作形式,其实就是一个对外开放的 Java Bean 或 Groovy Bean。例如,咱们相熟的 android{} 就是 Android Gradle Plugin 提供的扩大。

当你利用一个插件时,插件定义的扩大会以 扩展名-扩大对象 键值对的模式保留在 Project 中的 ExtensionContainer 容器中。插件内外部也是通过 ExtensionContainer 拜访扩大对象的。

注意事项:

  • 扩展名: 不反对在同一个 Project 上增加反复的扩展名;
  • 映射关系: 增加扩大后,不反对从新设置扩大对象;
  • DSL: 反对用 扩展名 {} DSL 的模式拜访扩大对象。

3.1 根本步骤

这一节咱们来讲实现 Extension 扩大的具体步骤,根本步骤分为 5 步:

  • 1、定义扩大类: 定义一个扩大配置类:

Upload.groovy

class Upload {    String name}
提醒: 依据 ”约定优先于配置“ 准则,尽量为配置提供默认值,或者保障配置缺省时也能失常执行。
  • 2、创立并增加扩大对象: 在 Plugin#apply() 中,将扩大对象增加到 Project 的 ExtensionContainer 容器中:

EasyUpload.groovy

class EasyUpload implements Plugin<Project> {    // 扩展名    public static final String UPLOAD_EXTENSION_NAME = "upload"    @Override    void apply(Project project) {        // 增加扩大        applyExtension(project)        // 增加 Maven 公布能力        applyMavenFeature(project)    }    private void applyExtension(Project project) {        // 创立扩大,并增加到 ExtensionContainer        project.extensions.create(UPLOAD_EXTENSION_NAME, Upload)    }    private void applyMavenFeature(Project project) {        // 构建逻辑    }}
  • 3、配置扩大: 应用方利用插件后,应用 扩展名 {} DSL定制插件行为:

build.gradle

apply plugin: 'com.pengxr.easyupload'upload {    name = "Peng"}
  • 4、应用扩大: 在 Plugin#apply() 中,通过 Project 的 ExtensionContainer 容器获取扩大对象,获取的代码倡议封装在扩大对象外部。例如:
class EasyUpload implements Plugin<Project> {    // 扩展名    public static final String UPLOAD_EXTENSION_NAME = "upload"    @Override    void apply(Project project) {        // 增加扩大        applyExtension(project)        // 增加 Maven 公布能力        applyMavenFeature(project)    }    private void applyExtension(Project project) {        // 创立扩大,并增加到 ExtensionContainer 容器        project.extensions.create(UPLOAD_EXTENSION_NAME, Upload)    }    private void applyMavenFeature(Project project) {        project.afterEvaluate {            // 1. Upload extension            Upload rootConfig = Upload.getConfig(project.rootProject)            // 构建逻辑 ...        }    }}

Upload.groovy

class Upload {    String name    // 将获取扩大对象的代码封装为静态方法    static Upload getConfig(Project project) {        // 从 ExtensionContainer 容器获取扩大对象        Upload extension = project.getExtensions().findByType(Upload.class)        // 配置缺省的时候,赋予默认值        if (null == extension) {            extension = new Upload()        }        return extension    }    /**     * 查看扩大配置是否无效     *     * @return true:valid     */    boolean checkParams() {        return true    }}
提醒: ExtensionContainer#create() 反对变长参数,反对调用扩大类带参数的构造函数,例如:project.extensions.create(UPLOAD_EXTENSION_NAME, Upload,"Name") 将调用构造函数 Upload(String str)
  • 5、构建逻辑: 到这里,实现插件扩大最根本的步骤就实现了,接下来就能够在 Plugin#apply 办法中持续实现你的表演。

3.2 project.afterEvaluate 的作用

应用插件扩大肯定会用到 project.afterEvaluate() 生命周期监听,这里解释一下:因为扩大配置代码的执行机会晚于 Plugin#apply() 的执行机会,所以如果不应用 project.afterEvaluate(),则在插件外部将无奈正确获取配置值。

project.afterEvaluate() 会在以后 Project 配置实现后回调,这个机会扩大配置代码曾经执行,在插件外部就能够正确获取配置值。

apply plugin: 'com.pengxr.easyupload'// 执行机会晚于 applyupload {    name = "Peng"}

3.3 嵌套扩大

在扩大类中组合另一个配置类的状况,咱们称为嵌套扩大,例如咱们相熟的 defaultConfig{} 就是一个嵌套扩大:

android {    compileSdkVersion 30    buildToolsVersion "30.0.0"    defaultConfig {        minSdkVersion 21        ...    }    ...}

默认下嵌套扩大是不反对应用闭包配置,咱们须要在内部扩大类中定义闭包函数。例如:

Upload.groovy

class Upload {    // 嵌套扩大    Maven maven    // 嵌套扩大    Pom pom    // 嵌套扩大闭包函数,办法名为 maven(办法名不肯定须要与属性名统一)    void maven(Action<Maven> action) {        action.execute(maven)    }    // 嵌套扩大闭包函数,办法名为 maven    void maven(Closure closure) {        ConfigureUtil.configure(closure, maven)    }    // 嵌套扩大闭包函数,办法名为 pom    void pom(Action<Pom> action) {        action.execute(pom)    }    // 嵌套扩大闭包函数,办法名为 pom    void pom(Closure closure) {        ConfigureUtil.configure(closure, pom)    }}

应用时:

build.gradle

apply plugin: 'com.pengxr.easyupload'upload {    maven {        ...    }}

3.4 NamedDomainObjectContainer 命名 DSL

在 Android 工程中,你肯定在 build.gradle 文件中见过以下配置:

build.gradle

android {    buildTypes {        release {            minifyEnabled false            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'        }        debug {            ...        }        // 反对任意命名        preview {            ...        }    }}

除了内置的 release 和 debug,咱们能够在 buildType 中定义任意多个且任意名称的类型,这个是如果实现的呢?—— 这背地是因为 buildTypes 是 NamedDomainObjectContainer 类型,源码体现:

com.android.build.api.dsl.CommonExtension.kt

val buildTypes: NamedDomainObjectContainer<BuildType>

NamedDomainObjectContainer 的作用:

NamedDomainObjectContainer<T> 直译是命名畛域对象容器,是一个反对配置不固定数量配置的容器。次要性能分为 3 点:

  • Set 容器: 反对增加多个 T 类型对象,并且不容许命名反复;
  • 命名 DSL: 反对以 DSL 的形式配置 T 类型对象,这也要求 T 类型必须带有 String name 属性,且必须带有以 String name 为参数的 public 构造函数;
  • SortSet 容器: 容器将保障元素以 name 天然程序排序。

那么,以上配置相当于以下伪代码:

build.gradle

val buildTypes : Collections<BuildType>BuildType release = BuildType("release")release.minifyEnabled = falserelease.proguardFiles = getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'BuildType debug = BuildType("debug")...BuildType preview = BuildType("preview")...buildTypes.add(release)buildTypes.add(debug)buildType.add(preview)

NamedDomainObjectContainer 的用法:

这里介绍一下具体用法,咱们仅以你相熟的 BuildType 为例,但不等于以下为源码。

  • 1、定义类型 T: 在类型 T 中必须带有以 String name 为参数的 public 构造函数。例如:

BuildType.groovy

class BuildType {    // 必须有 String name 属性,且不容许结构后批改    @Nonnull    public final String name    // 业务参数    boolean minifyEnabled    BuildType(String name) {        this.name = name    }}
  • 2、定义 NamedDomainObjectContainer 属性: 在扩大类中定义一个 NamedDomainObjectContainer<BuildType> 类型属性。例如:

CommonExtension.grooyv

class CommonExtension {    NamedDomainObjectContainer<BuildType> buildTypes    CommonExtension(Project project) {        // 通过 project.container(...) 办法创立 NamedDomainObjectContainer        NamedDomainObjectContainer<BuildType> buildTypeObjs = project.container(BuildType)        buildTypes = buildTypeObjs    }    // 嵌套扩大闭包函数,办法名为 buildTypes    void buildTypes(Action<NamedDomainObjectContainer<BuildType>> action) {        action.execute(buildTypes)    }    void buildTypes(Closure closure) {        ConfigureUtil.configure(closure, buildTypes)    }}
  • 3、创立 Extension: 依照 4.1 节介绍的步骤创立扩大。例如:
project.extensions.create("android", CommonExtension)

到这里,就能够依照 buildTypes {} 的形式配置 BuildType 列表了。然而,你会发现每个配置项必须应用 = 进行赋值。这就有点膈应人了,有懂的大佬领导一下。

android {    buildTypes {        release {            // 怎样才能省略 = 号呢?            minifyEnabled = false            proguardFiles = getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'        }    }}

4. 插件调试

4.1 两个调试办法

在开发插件的过程肯定须要调试,除了通过日志调试,咱们也有断点调试的需要。这里总结两个办法:办法 1 尽管只反对调试简略执行工作,但曾经能满足大部分需要,而且绝对简略。而办法 2 反对命令行增加参数。

办法 1(简略): 间接提供 Android Studio 中 Gradle 面板的调试性能,即可调试插件。如下图,咱们抉择与插件性能相干的 Task,并右键抉择 Debug 执行。

办法 2: 通过配置 IDE Configuration 以反对调试命令行工作,具体步骤:

  • 1、创立 Remote 类型 Configuration:

  • 2、执行命令: ./gradlew Task -Dorg.gradle.debug=true --no-daemon (开启 Debug & 不应用守护过程),执行后命令行会进入期待状态:

  • 3、Attach Debug: 点击调试按钮,即可开始断点调试。

4.2 调试技巧

一些调试技巧:

  • 援用插件源码: 在开发阶段能够间接本地依赖插件源码,而不须要将插件公布到 Maven 仓库,只须要在 build.gradle 文件中批改配置:

我的项目 build.gradle

buildscript {    repositories {        google()        jcenter()    }    dependencies {        // For Debug        classpath project(":easyupload")        // classpath "com.pengxr:easyupload:1.0.0"    }    ...}
  • 插件代码开关: 因为 Plugin#apply 中的代码在配置阶段执行,如果其中的代码有问题就会呈现 Sync 报错。又因为编译插件代码须要先 Sync,只能先将工程中所有应用插件的代码正文掉,从新编译插件模块,再将正文批改回来。真麻烦!咱们还是加一个开关吧,例如:

gradle.properties

ENABLED=true

模块 build.gradle

if (ENABLED.toBoolean()) {    apply plugin: 'com.pengxr.easyupload'    upload {        name = "123"    }}

5. 插件开发技巧总结

  • 判断是否以后是 App 模块还是 Library 模块: 当咱们开发 Android 我的项目相干插件时,常常须要依据插件的应用环境辨别不同逻辑。例如插件利用在 App 模块和 Library 模块会采纳不同逻辑。此时,咱们能够用在 Plugin#apply() 中采纳以下判断:
project.afterEvaluate {    // 1. Check if apply the ‘com.android.application’ plugin    if (!project.getPluginManager().hasPlugin("com.android.application")) {        return    }}
  • 插件开发语言: 最后,Groovy 是 Gradle 的首要语言,但随着 Java 和 Kotlin 语言的演进,这一现状有所扭转。当初的趋势是:Gradle 脚本应用 Groovy 或 Kotlin 开发,而 Gradle 插件应用 Kotlin 开发。例如,咱们能够发现 AGP 当初曾经用 Kotlin 开发了。尽管趋势是往 Kotlin 靠,但目前存量的 Gradle 脚本 / 插件还是以 Groovy 为主。

    • Groovy 劣势:社区积淀、动静语言
    • Kotlin 劣势:IDE 反对、趋势
原文: In general, a plugin implemented using Java or Kotlin, which are statically typed, will perform better than the same plugin implemented using Groovy.

6. 总结

到这里,Gradle 插件的局部就讲完了,须要 Demo 的同学能够看下咱们之前实现过的小插件: EasyPrivacy。在本系列后续的文章中,也会有新的插件 Demo。关注我,带你理解更多,咱们下次见。

参考资料

  • 《实战 Gradle》—— [美] Benjamin Muschko 著,李建 朱本威 杨柳 译
  • 《Gradle for Android》—— [美] Kevin Pelgrims 著,余小乐 译
  • Groovy 参考文档 —— Groovy 官网文档
  • Gradle 阐明文档 —— Gradle 官网文档
  • Gradle DSL 参考文档 —— Gradle 官网文档
  • Developing Custom Gradle Plugins —— Gradle 官网文档
  • Using Gradle Plugins —— Gradle 官网文档
  • 深刻摸索 Gradle 自动化构建技术(系列) —— jsonchao 著
你的点赞对我意义重大!微信搜寻公众号 [彭旭锐],心愿大家能够一起探讨技术,找到气味相投的敌人,咱们下次见!