前言
在之前,我们提到 Android 项目的构建需要 Android Gradle 插件来完成。每一个 Gradle 插件就是来帮助任务进行构建的,在你的 build.gradle 文件中应用你的插件并编写构建 DSL (domain specific language)脚本。本章将给你一个直观的插件编写例子,以及 Android Gradle 插件的相关介绍。
1、插件开发
插件代码可以放在下面几个地方,或者说,使用一个插件有三种方式:
- Build script,把插件源代码放在构建脚本 build.gradle 中。插件会被自动编译,并加入到构建脚本的 classpath 中。缺点是对外部构建脚本(build script)不可见,不能复用。
- buildSrc 项目,把插件源代码放在 rootProjectDir/buildSrc/src/main/java 目录下。需要编译和测试才可用,本项目构建的构建脚本都可以使用。然而,对于外部构建不可用。
- 独立项目,打包发布成一个 JAR 包的方式。可以被多个构建复用。
本示例中为第二种方式。
1.1、创建项目
创建插件工程并切换到项目目录下:
|
|
生成插件代码目录:
|
|
1.2、创建插件
在刚创建的目录下生成 GreetingPlugin 类,该类必须实现 Plugin 接口。当插件被工程使用时,Gradle 会生成一个 plugin 类实例并调用实例的 aplly(T) 方法。
buildSrc/src/main/java/org/example/greeting/GreetingPlugin.java
|
|
其中,生成了一个命名为 hello 的 Greeting 类型的新任务实例,并为之设置了默认值。
这就是一个真实的插件代码,并且 Project 对象是全部 Gradle API 的访问入口,通过它可以完成在构建脚本所做的同样工作。在本例中,在目标项目中创建了一个 hello 任务。
接下来,为 GreetingPlugin 使用的任务类型生成 java 类。依旧在改包路径下添加 Greeting 类。
buildSrc/src/main/java/org/example/greeting/Greeting.java
|
|
该类中有一些属性以及 getter/setter 类。在 sayGreeting 方法中指定了当任务运行后输出的配置消息。
1.3、使用插件
有了上面的工作,现在就实现了一个简单的插件了。下面,你可以使用插件(当然,比较low一点,只能打印一个输出语句)。在 build.gradle 文件中使用 apply 关键字在项目中使用插件:
|
|
这样,当前的 Project 实例将使用改插件,并在构建中添加一个 hello 任务。
使用 gradle hello 命令来执行 hello 任务来验证插件的正确性:
|
|
控制台的输出表面 buildSrc 中的文件被当作了 Java 项目现进行了一个编译过程。然后在主构建中进行了执行。
现在构建中 hello 任务使用的是默认值,你也可以在 build 脚本中进行一个配置:
|
|
再次执行 hello 任务,并使用 -q 选项去掉一些控制台输出:
|
|
现在,自定义插件功能运行 OK。接下来,我们为插件声明一个ID,同时,当你发布插件时这样做会有帮助。
1.4、声明插件ID
通常情况下,我们都是通过 ID 来使用插件,这样比较容易记忆。
首先,生成属性文件:
buildSrc/src/main/resources/META-INF/gradle-plugins/org.example.greeting.properties
|
|
Gradle 会根据改文件来确定 Plugin 接口的实现类。改属性文件名去掉 .properties 就是插件的 ID。
然后,在 build 脚本中,对下面的内容进行替换:
|
|
使用插件 ID:
|
|
1.5、插件配置
多数插件需要从构建脚本中获取一些配置。其中一个方式就是使用 extension (扩展)对象。Gradle Project 有一个相关的 ExtensionContainer
对象,它存放所有的配置及属性。你可以在其中添加扩展来为插件提供配置。一个扩展对象其实就是一个 Java Bean。
配置可以提供一些丰富的属性,在 Android 开发中,这很常见:
|
|
Android Gradle Plugin 在 build 脚本中主要的就是这个 android 扩展配置。插件通过该扩展来进行构建任务的相关配置。
这里,你可以参考:
- 自定义插件编写(官方) Writing Custom Plugins ,该指南是基于 Groovy 语言编写的。
- Gradle插件开发 ,该份文章你会对自定义插件开发有一个更好的概念上的了解,尤其是对 task 的增量构建方面,扩展的编写。
- A simple Gradle Plugin development example ,本人在 Github 上的一个简单的插件编写示例,基于 Java 语言实现的。
1.6、发布插件
开发的插件可以发布到 Gralde Portal 上,和其他插件一样共享给其他人使用。
本人把前面那个插件工程从 buildSrc 中抽离了出来,建立了一个单独的插件工程,里面有上传的配置 buidl.gradle 文件,可以供你参考。
本人插件的源码及完整发布配置示例 https://github.com/whtacm/GradlePluginSrc
在这之前,你需要到 registration page 上去注册账号并申请一个 API Key,然后配置到你的 ~./gradle/gradle.properties 文件中去。
然后编写自己的插件代码并配置相关的信息,执行:
|
|
可以看到发布成功了,到 https://plugins.gradle.org/plugin/org.robin.example.greeting 页面上可以确认该插件上传成功并且版本一致。最后,在你的其他工程中使用该插件:
|
|
执行
|
|
控制台的输出表面插件运行正确,在该目录下生成了相关内容。
1.7、进一步阅读
- 使用Java Gradle Plugin 开发插件简化插件开发 Simplifying plugin development with the Java Gradle Plugin Development Plugin
- 发布插件到 Gradle Plugin Portal Publishing plugins to the Gradle Plugin Portal
- 使用扩展进行领域建模 Modeling your domain with extensions
- 插件测试 Testing plugins
- 为新任务类型添加持续构建支持 Adding incremental build support to new task types
2、Android Gradle Plugin
刚接触 Android Studio 的时候,一直不太了解 build.gradle 里的那些 DSL,诸如 buildscript、allprojects、android 太多的标签。当然,你可以通过这份官方文档来学习 Android Plugin DSL Reference 。后来,直到我看了 Android Gradle Plugin的代码之后我才对这些东西有了更清晰的认识。因此,为什么要了解 Android Gradle Plugin,其实是一个 知其所以然 的过程。
2.1、从 Project 开始
我们知道,Android Stutio 中一个工程有多个模块,工程目录以及模块目录下都有一个 build.gradle 文件。工程目录下的构建脚本:
|
|
其中,依赖里的 classpath ‘com.android.tools.build:gradle:2.3.1’ 就是使用的 Android Gradle Plugin 版本。在构建时候会下载该插件版本进行构建工作。
而 buildscript 、dependencies 这些 DSL 都由 Gradle 来实现,其中 buildscript 可以定位到 org.gradle.api.Project 的 void buildscript(Closure configureClosure) 方法。该脚本主要是配置 project 的类路径,并由 org.gradle.api.initialization.dsl.ScriptHandler 执行。
此外,通过查看 Project 类,你还可以看到很多的方法,比如: void allprojects(Closure configureClosure)、Map
在前面的内容中,我们看到,要实现一个插件,需要实现 Plugin 接口的 apply 方法。当你使用插件时候,apply 被调用并传递一个 Project 类的对象。通过该对象就可以使用上面的这些方法,通过 getProperties() 方法能够获取很多的相关属性和对象。这里有很多,包括配置在 ~/.gradle/gradle/properties 中的属性信息。
2.2、BasePlugin及其子类
一般的在我们的 app 模块的 build.gradle 文件中开头需要这么写:
|
|
从前面的内容,我们知道该模块使用了 ID 为 ‘com.android.application’ 的插件类。那么,它的具体实现类是哪个一个呢?我们可以在 build.gradle 文件中添加:
|
|
然后 sync 一下,就可以在 External Libraries 下面找到该包。该包目录:
|
|
OK,根据前面的内容,显而易见的打开 com.android.application.properties 文件就知道了该实现类是 :
|
|
那么,library 模块插件实现类,你也知道了:
|
|
AppPlugin 类及 LibraryPlugin 类都继承了 BasePlugin 这个抽象类,并且都实现了 Plugin 接口。主要的工作是在 BasePlugin 中完成的,那么我们来看看这个类的主要工作。
首先,我们来看入口 apply(Project) 方法:
|
|
其中,比较重要的是调用了configureProject、 configureExtension 以及 createTasks 这三个方法。从字面上,比较容易理解它们可能完成的工作。
我们先看 configureProject 方法:
|
|
configureProject 的主要工作是下载依赖、对任务之间的缓存处理以及构建完成后的记录动作。值得注意的是这个 cache.xml 文件的路径是在工程根目录下的 build/intermediates/dex-cache 下面。除此之外,工程目录的 build 目录下存放了很多文件,比如 混淆文件、profile 报告。同时,在每个模块下面的 build 中也有大量的文件,都是在构建过程中生成的缓存等。
configureExtension 的代码:
|
|
该方法,我们在本篇主要关心 createExtension 方法。在 app 模块中,该方法由 AppPlugin 实现:
|
|
OK,到这里你也许意识到了,在 app 模块下的 build.gradle 文件中的 android{} 扩展名其实是在这里给定了。通过 project.getExtensions().create(…) 可以向容器中添加一个名为 android 的扩展,扩展类型为 AppExtension 。该类的传入参数包括 buildTypeContainer、productFlavorContainer、signingConfigContainer,它们中包括了构建类型、产品风味以及签名的配置等等。
最后,我们来看 createTasks 方法:
|
|
该方法中,我们重点看它调用的 createAndroidTasks 方法,它又调用了 VariantManager 的 createAndroidTasks ,该方法又调用了 createTasksForVariantData 方法。OK,这里该方法的实现类,对于 app 模块而言是 ApplicationTaskManager 类。
最后总结一下,configureProject、 configureExtension 以及 createTasks 这三个方法相互配合完成了工程的初始化、扩展配置的信息以及任务的创建工作。接下来,我们来了解下扩展相关的内容。
2.3、BaseExtension及其子类
从前面的介绍中,我们了解到 buidl.gradle 中的 android{} 对应于 AppExtension 类。该类是 BaseExtension 抽象类的子类,此外, LibraryExtension、TestExtension 也是 BaseExtension 的子类,它们对应于插件 com.android.library 、com.android.test。
这是一个 app 模块的 buidl.gradle 文件:
|
|
它对应于 AppExtension 类,这里我们主要看 BaseExtension 类,这里几乎可以找到一切的实现。首先,我们来看构造函数:
|
|
这里,我们关注下 buildTypes、productFlavors 、signingConfigs、defaultConfig 这一些,它们在构建脚本对应于一些对象(即带有 {} 的脚本块)。而不像脚本中的 compileSdkVersion、buildToolsVersion,它们只是属性。这些属性在 BaseExtension 中只有 getter/setter,而 buildTypes、productFlavors 、signingConfigs、defaultConfig 这一些还需要类似下面的方法,比如 productFlavors :
|
|
这是因为在调用 setter 方法时候,像 compileSdkVersion 一类的传递的是简单属性值,而 productFlavors 这一类的传递的是一个块(或者说是对象)。因此,需要 Action 的接口来处理该情况。
另外,在 buildTypes、productFlavors 、signingConfigs 里可以创建指定类型的命名对象(块),就像上面的 app 模块中的 buidle.gradle 中的 productFlavors:
|
|
其中,flavor1 和 flavor2 都属于 ProductFlavor 类型,但它们名字不同。这一特性由 NamedDomainObjectContainer 和其工厂类 ProductFlavorFactory 来完成。而 ProductFlavor 是一个实体类,它的构造函数类似 BaseExtension 。当然,属于 ProductFlavor 类型的 flavor1 里面可以包含了一些属性和脚本块 (比如 ndk{})。
整个扩展里就是属性和脚本块的组合,它给整个项目提供了丰富的配置信息,这些信息被各种任务获取来完成如变体等配置处理。