手把手教你写 Gradle 插件 | 数据采集

一、前言

在上一篇文章《神策 Android 全埋点插件介绍》中,我们了解到神策 Android 插件其实是自定义的 Gradle 插件。Gradle 是一个专注于灵活性和性能的开源自动化构建工具,而插件的作用在于打包模块化的、可重用的构建逻辑。可以通过插件实现特定的逻辑,并打包起来分享给别人使用。例如:神策 Android 全埋点插件正是通过插件在编译时对特定函数进行处理,从而实现控件点击和 Fragment 页面浏览全埋点的采集。

本文我们会先针对 Gradle 的基础知识作一定的介绍,再举例说明如何实现一个自定义的 Gradle 插件。这里需要注意的是:文中采用 ./gradlew 去执行 Gradle 的命令,如果是 Windows 用户的话需要改成 gradlew.bat。

二、Gradle 基础

Gradle 有两个重要的概念:Project 和 Task,本节将会介绍它们各自的作用以及之间的关系。

2.1 Project 简介

Project 是与 Gradle 交互中最重要的 API,我们可以通过 Android Studio 的项目结构来理解 Project 的含义,如图 2-1 所示:


图 2-1 Android Studio 项目结构图

图 2-1 是写作过程中使用到的一个项目(名为 BlogDemo),包含 app 和 plugin 这两个 Module。这里不管是 “项目” 还是 “Module” 在构建时都会被 Gradle 抽象成 Project 对象。它们的主要关系是:

1、Android Studio 结构中的项目相当于一个父 Project,而一个项目中所有的 Module 都是该父 Project 的子 Project;

2、每个 Project 都会对应一个 build.gradle 配置文件,因此使用 Android Studio 创建一个项目的时候在根目录下有一个 build.gradle 文件,在每个 Module 的目录下又各有一个 build.gradle 文件;

3、Gradle 是通过 settings.gradle 文件去进行多项目构建,从图 2-1 中也可以看出项目之间的关系。

父 Project 对象可以获取到所有的子 Project 对象,这样就可以在父 Project 对应的 build.gradle 文件中做一些统一的配置,例如:管理依赖的 Maven 中心库:

...
allprojects {
    repositories {
        google()
        jcenter()
    }
}
...

2.2 Task 简介

Project 在构建过程中会执行一系列的 Task。Task 的中文翻译是 “任务”,它的作用其实也就是抽象了一系列有意义的任务,用 Gradle 官方的话说就是:Each task perform some basic work。例如:当你点击 Android Studio 的 Run 按钮的时候,Android Studio 会把项目编译、运行,实际上这个过程就是执行了一系列的 Task 来完成的。可能包含:编译 Java 源码的 Task、编译 Android 资源的 Task、编译 JNI 的 Task、混淆的 Task、生成 Apk 文件的 Task、运行 App 的 Task 等。也可以在 Android Studio 的 Build Output 看到真正运行的是哪些 Task,如图 2-2 所示:

图 2-2 Android Studio Build 输出日志

从图中右侧我们可以看到,Task 由两个部分组成:Task 所在的 Module名和 Task 的名称。在运行 Task 的时候,也需要按照这样的方式去指定一个 Task。

另外,可以自定义实现自己的 Task,我们来创建一个最简单的 Task:

// add to build.gradle
task hello {
    println 'Hello World!'
}

这段代码的含义是创建了一个名为 “hello” 的 Task,想要单独执行该 Task 的话,可以在 Android Studio 的 Terminal 中输入 “./gradlew hello”,执行后就可以看到控制台输出 “Hello World!”。

三、Gradle 插件构建

3.1 Plugin 简介

Plugin 和 Task 从它们的作用来看其实区别不大,都是把一些业务逻辑封装起来,Plugin 适用的场景是打包需要复用的编译逻辑(即把一部分编译逻辑模块化出来)。可以自定义 Gradle 插件,实现必要的逻辑后把它发布到远程仓库或者打成本地 JAR 包分享出去。这样,后续想要再次使用它或者想分享给别人使用的时候,就可以直接引用远程仓库的包或者引用本地的 JAR 包。

最常见的 Plugin 应该就是 Android 官方提供的 Android Gradle Plugin。可以在项目主 Module 的 build.gradle 文件第一行看到:“apply plugin: 'com.android.application'”,这即是 Android Gradle Plugin。“com.android.application” 指的是 plugin id,该插件的作用是帮助你生成一个可运行的 APK 文件。

插件还可以读取写在 build.gradle 文件中的配置。主 Module 的 build.gradle 文件中会有一个名为 “android” 的块,块中定义了一些属性,例如:App 支持的最低系统版本、App 的版本号等。你可以把这里的 “android”android 块类比成数据类或者基类,定义的属性类比成类的成员变量。Android Gradle Plugin 在运行时可以拿到 “android” 块实例化的对象,进而根据对象的属性值运行不同的编译逻辑。

3.2 构建独立项目的 Gradle 插件

Gradle 插件有三种实现方式,分别为 Build script、buildSrc project 和 Standalone project:

1、Build script 会把逻辑直接写在 build.gradle 文件中,Plugin 只对当前 build.gradle 文件可见;

2、buildSrc project 是将逻辑写在 rootProjectDir/buildSrc/src/main/java(最后一个路径文件夹也可以是 groovy 或 kotlin,主要取决于你用什么语言去实现自定义插件) 目录下,Plugin 只对当前项目生效;

3、Standalone project 是将逻辑写在独立项目里,可以直接编译后把 JAR 包发布到远程仓库或者本地。

基于本文的写作目的,这里我们主要讲解 Standalone project,即独立项目的 Gradle 插件。

3.2.1 目录结构解析

一个独立项目的 Gradle 插件大致结构如图 3-1 所示:

图 3-1 Gradle 插件项目目录示意图

在 main 文件夹下分为 groovy 文件夹与 resources 文件夹:

  • groovy 文件夹下是源码文件(Gradle 插件也支持使用 Java 与 Kotlin 编写,此处文件夹名根据实际语言确定);
  • resources 文件夹下面是资源文件。

其中,resources 文件夹下是固定格式的 META-INF/gradle-plugins/XXXX.properties,XXXX 就代表以后使用插件时需要指定的 plugin id。

目前 Android Studio 对于 Gradle 插件开发的支持不够好,很多 IDE 本可以完成的工作都需要我们手动完成,例如:

1、Android Studio 不能够直接新建 Gradle 插件的 Module,只能先新建一个 Java Library 类型的 Module,再把多余的文件夹删除;

2、新建类默认是新建 Java 的类,新建的文件名后缀是 “.java”,想要新建 Groovy 语法的类需要手动新建一个后缀为 “.groovy” 的文件,然后添加上 package、class 声明;

3、resources 整个都需要手动创建,文件夹名需要注意拼写;

4、删除掉 Module 的 build.gradle 全部内容,新加上 Gradle 插件开发需要的 Gradle 插件、依赖等。

3.2.2 编写插件

在写插件的代码之前,我们需要对 build.gradle 做些修改,如下所示:

apply plugin: 'groovy'
apply plugin: 'maven'
 
dependencies {
    implementation gradleApi()
    implementation localGroovy()
}
 
uploadArchives{
    repositories.mavenDeployer {
        //本地仓库路径,以放到项目根目录下的 repo 的文件夹为例
        repository(url: uri('../repo'))
        //groupId ,自行定义
        pom.groupId = 'com.sensorsdata.myplugin'
        //artifactId
        pom.artifactId = 'MyPlugin'
        //插件版本号
        pom.version = '1.0.0'
    }
}

这里主要分为三部分内容:

1、apply 插件:应用 'groovy' 插件是因为我们的项目是使用 Groovy 语言开发的,'maven' 插件在后面发布插件时会用到;

2、dependencies:声明依赖;

3、uploadArchive:这里是一些 maven 相关的配置,包括发布仓库的位置、groupId、artifactId、版本号,这里为了调试方便把位置选在项目根目录下的 repo 文件夹。

做好以上准备之后,就可以开始源码的编写。Gradle 插件要求入口类需要实现 org.gradle.api.Plugin 接口,然后在实现方法 apply 中实现自己的逻辑:

package com.sensorsdata.plugin
class MyPlugin implements Plugin<Project>{
    @Override
    void apply(Project project) {
        println 'Hello,World!'
    }
}

在这里的示例中,apply 方法就是我们整个 Gradle 插件的入口方法,作用类似于各种语言的 main 方法。apply 方法的入参类型 Project 在第二节中已经进行了解释,这里不再赘述。由于 Plugin 类和 Project 类有非常多的同名类,在导入的时候一定注意选择 org.gradle.api 包下的类。

最后,还需要做一项准备工作:Gradle 插件并不会自动寻找入口类,而是要求开发者把入口类的类名写在 resources/META-INF/gradle-plugins/XXXX.properties 里,内容格式为 “implementation-class=入口类的全限定名”,此处示例项目的配置如下所示:

// com.sensorsdata.plugin.properties
implementation-class=com.sensorsdata.plugin.MyPlugin

3.2.3 发布插件

完成编写插件的所有内容后,在终端执行

./gradlew uploadArchive

就可以发布插件。在上一小节编写插件的 build.gradle 文件中提前配置好了发布到 maven 仓库相关的配置,因此我们这里执行该命令后,在项目根目录下就会出现 repo 文件夹,文件夹中包含打包后的 JAR 文件。

3.2.4 使用插件

使用插件主要分别两个步骤:

(1)声明插件

声明插件需要在 Project 级别的 build.gradle 文件中完成,在 build.gradle 文件中有一个块叫做 buildscript,buildscript 块又分为 repositories 块和 dependencies 块。repositories 块用来声明需要引用的依赖所在的远程仓库地址,dependencies 块用来声明具体引用的依赖。这里使用刚刚发布到本地 repo 文件夹 JAR 包为例,参考代码如下:

buildscript {
    repositories {
        maven{
            // 刚刚我们把插件发布到了根目录下面的 repo 文件夹
            url 'repo'
        }
    }
    dependencies {
        // classpath '$group_id:$artifactId:$version'
        classpath 'com.sensorsdata.myplugin:MyPlugin:1.0.0'
    }
}

(2)应用插件

应用插件需要在 Module 级别的 build.gradle 文件中完成:

// apply plugin: 'plugin id'
apply plugin: 'com.sensorsdata.plugin'

完成上述步骤之后,在每次编译的时候都可以在编译日志中看到插件输出的 “Hello,World!”。

3.3 可配置的插件

如果希望插件的功能更加灵活的话,一般会预留一些可配置的参数,就像可以在主 Module 的 “android” 块配置编译的 Android SDK 版本、Build-Tools 版本等。“android” 块的这个配置就是 Gradle 的 Extension,下面我们来做一个自定义的 Extension。

3.3.1 创建 Extension 类

创建一个用于 Extension 的类非常简单:只需要新建一个普通的类,类中定义的属性就是 Extension 可以接收的配置。它不需要继承任何类,也不需要实现任何接口,如下所示:

class MyExtension{
    public String nam = "name"
    public String sur = "surname"
}

3.3.2 实例化 Extension 对象

可以通过 ExtensionContainer 来创建和管理 Extension,ExtensionContainer 对象可以通过 Project 对象的 getExtensions 方法获取:

def extension = project.getExtensions().create('myExt',MyExtension)
project.afterEvaluate {
    println("Hello from " + extension.toString())
}

上面的代码片段可以直接复制到 apply 方法中或者放在 build.gradle 文件中使用。这里使用到了 create 方法来创建 Extension,我们来看下 create 方法的定义:

<T> T create(String name, Class<T> type, Object... constructionArguments);

1、name:代表要创建的 Extension 的名字,例如:build.gradle 中名为 “android” 的块,Android Gradle 插件在创建这个 Extension 的时候 name 就需要填 “android”。Extension 的 name 不能和已有的重复,例如: Android Gradle 插件创建的 Extension name 为 “android”,那么其它 Extension name 就不可以再使用 “android”;

2、type:该 Extension 的类类型,这里的类就是上一小节创建的类,注意类的属性名与 Extension 中的属性名需要一致;

3、constructionArguments:类的构造函数参数值。

使用 create 方法之后,你可能会迫不及待的在下一行立即打印出 Extension 对象的值,不过这么做的话你会发现 Extension 对象打印出来的值并不对。不论你在 build.gradle 中怎么配置,Extension 对象就是读不到值。具体原因可以回顾下这里的示例,你会发现示例里打印的逻辑写在了afterEvaluate 方法中。这里的写法跟插件的生命周期有很大的关系,我们将在下一节中介绍 Gradle 插件的生命周期。

四、Gradle 构建的生命周期

官方对于 Gradle 构建的生命周期的定义:Gradle 的核心是一种基于依赖的语言,用 Gradle 的术语来说这意味着你能够定义 Task 和 Task 之间的依赖关系。Gradle 会保证这些 Task 按照依赖关系的顺序执行并且每个 Task 只会被执行一次,这些 Task 根据依赖关系构成了一个有向无环图。Gradle 在执行任何 Task 之前都会用内部的构建工具去完成这样这样一个图,这就是 Gradle 的核心。这种设计使得很多原本不可能的事情成为可能。

每次 Gradle 构建都需要经过三个不同的阶段:

1、初始化阶段:Gradle 是支持单项目和多项目构建的,因此在初始化阶段,Gradle 会根据 settings.gradle 确定需要参与构建的项目,并为每个项目创建一个 Project 实例。Android Studio 的项目和 Module 对 Gradle 来说都是项目;

2、配置阶段:在这个阶段会配置 Project 对象,并且所有项目的构建脚本都会被执行。例如:Extension 对象、Task 对象等都是在这个阶段被放到 Project 对象里;

3、执行阶段:经过了配置阶段,此时所有的 Task 对象都在 Project 对象中。然后,会根据终端命令指定的 Task 名字从 Project 对象中寻找对应的 Task 对象并执行。

Gradle 提供了很多生命周期的监听方法,用来在特定的阶段执行特定的任务。这里选取了部分回调方法,按照执行顺序关系画了一份 Gradle 生命周期流程简图,如图 4-1 所示:

图 4-1 Gradle 生命周期流程简图

图中的生命周期回调方法里,属于 Project 有 project.beforeEvaluate 和 project.afterEvaluate,它们的触发时机分别是在 Project 进行配置前和配置结束后。在之前的示例中,正是使用了这里的 afterEvaluate,由于方法的最后一个参数是闭包所以写法优化成了 afterEvaluate{}。

使用 create 创建的对象,在对应 Project 还没配置完成的时候打印出来的值自然是不正确的,需要在配置完成后才能正确获取到写在 build.gradle 中的 Extension 值。因为直接写在 apply 方法里的逻辑是在配置阶段执行的,所以会出现这种情况。

五、总结

本文首先对 Gradle 的基础知识做了一定的介绍,包含 Project 与 Task,然后重点讲解了自定义 Gradle 插件从创建到使用的详细过程。希望能够为编写自定义的 Gradle 插件提供一定的帮助。

本文作者

顾鑫 神策数据 |SDK 技术顾问

我是顾鑫,神策数据 Android 技术顾问。神策数据是我就职的第一家公司,他非常的棒~学习中我喜欢做 Android 相关开发,也喜欢接触新兴技术,希望在开源社区能与你共同学习、共同进步。

本文著作权归「神策数据开源社区」所有,商业转载请联系我们获得授权;非商业转载请注明出处,并附上神策数据开源社区服务号二维码。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350