乐趣区

关于android:手把手带你自定义-Gradle-插件-Gradle-系列2

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

🔥 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: MyCustomGradlePlugin

class 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 :buildSrc
build.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 中定义的插件 ID
apply 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'

// 执行机会晚于 apply
upload {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 = false
release.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 著

你的点赞对我意义重大!微信搜寻公众号 [彭旭锐],心愿大家能够一起探讨技术,找到气味相投的敌人,咱们下次见!

退出移动版