在前六个系列中,主要介绍了Gradle常规的使用实践和一些基础属性,这一部分将会更深入的介绍gradle的task和plugin部分,主要包括如下内容:
- 理解Groovy
- 自定义Tasks
- 深入Android plugin
- 定义自己的plugin
理解Groovy
大部分Android的开发者都是使用Java作为开发语言,groovy跟java相较而言并无多大区别,反而更易读。接下来这部分主要对groovy做一个简要的使用介绍
更多关于Groovy的使用,可以访问Goovy官网查看更多文档
关于Groovy
Goovy是继承自Java,运行在JVM上的一门脚本语言,它的宗旨是更简单,更通俗易通。接下来我们通过对比Java和Groovy语言实现上的异同来了解Groovy是如何工作的
在Java中,打印一个字符串在屏幕上如下代码实现
1 | System.out.println("Hello, world!"); |
在Groovy中,如下
1 | println 'Hello, world!' |
可以看到groovy的实现有如下不同
- 没有System.out的命名空间
- 没有小括弧包裹方法参数
- 没有分号在代码行末尾
上面的例子中groovy的字符串参数使用单引号引用,在groovy中单引号和双引号都可以使用,但是某些地方有些微的不同,双引号的字符可以使用占位符,如下为使用字符串的一些例子
1 | def name = 'Andy' |
grreting值为 “hello,Andy” ,name_size的值为 “Your name is 4 characters long.”
字符的变量引用还支持动态的方法运行
1 | def method = 'toString' |
上面的这种写法在Java中可能觉得很奇怪,但是在像Groovy这种动态语言,这种写法很常见
Groovy中的类和成员变量
Groovy中新建类与Java类似,如下为一个简单的Groovy类包含一个成员变量和方法
1 | class MyGroovyClass { |
可以看到上面的类,方法和成员变量都没有像 public或者private 这样的限制,不像Java中默认的访问限制,Groovy中的class和method默认是public,成员变量是private,使用 MyGroovyClass 类,如下所示:
1 | def instance = new MyGroovyClass() |
通过使用关键词 def 创建变量,如上创建新的MyGroovyClass对象后,就可以通过instance访问getGreeting()方法,Groovy默认会给成员变量创建get和set方法,如
1 | println instance.getGreeting() |
上面两行代码调用其实是一样的instance.greeting实际上也是调用的instance.getGreeting()方法
方法
方法的定义groovy与java有两点不同
- groovy不强制要求定义方法的返回类型
- groovy总是会在方法结束加上return语句
详细可参考如下对比
Java代码如下
1 | public int square(int num) { |
Groovy代码如下
1 | def square(def num) { |
通过两段代码比较,可以看到groovy代码没有int的方法返回值限制,也没有return语句(但是在实际中,为了代码的易读性,应该加上return语句),在调用方法的时候参数没有圆括号包裹。
还有一种更简洁的方法声明,可参考如下代码
1 | def square = { num -> |
这种方式不是标准的方法定义,而是闭包的一种实现,在Java中没有闭包的概念,但是在Groovy中闭包占据着重要的地位。
闭包
闭包是一个能够接收参数也能返回结果匿名的方法块。它能被赋值到一个变量,也能当做一个参数传递。
闭包的定义可以如上一个代码块演示,也可以更简洁一点
1 | Closure square = { |
在声明的时候可以使用 def 关键字,使用 Closure 可以让代码更清晰,在闭包中使用的参数,如果不声明,groovy默认使用 it 代替,但是只能在闭包只有一个参数的时候使用,如果使用it,但是调用的时候没有给闭包传入参数,那么it则为null
在Gradle中,大部分代码的实现几乎都是以闭包的方式实现的,像 android{} , dependencies{} 块其实都是闭包的实现
集合
在Gradle中使用Groovy的集合主要有两种:List和Map
创建List如下所示
1 | List list = [1, 2, 3, 4, 5] |
遍历list集合也很简单
1 | list.each(){element -> |
each 方法可以迭代list中的每一个数据,通过it参数还可以让上面的方法更精简
1 | list.each() { |
Map类型在Gradle的配置中使用的较多,定义map如下所示
1 | Map pizzaPrices = |
获取map中的值可以通过get方法或者方括号引用
1 | pizzaPrices.get('pepperoni') |
map也支持一种更简洁的方法来取值,如下所示
1 | pizzaPrices.pepperoni |
Groovy在Gradle中的使用
通过连接groovy的一些基础概念,可以让我们更好的理解Gradle中配置代码的含义,如
1 | apply plugin: 'com.android.application' |
这行代码的意义,在不简写的情况下其实是这样的
1 | project.apply([plugin: 'com.android.application']) |
apply是project类的一个方法,参数是一个map参数,map中只用一个数据,key是“plugin”,value是“com.android.application”
还可以看到依赖的配置
1 | dependencies { |
从上代码可知,这个代码块是一个闭包,在project的dependencies方法中,不简写的代码如下所示
1 | project.dependencies({ |
从上代码可知dependencies传进来的闭包交给 DependencyHandler 类的add方法中,add方法接受一个配置名称(“compile”)和依赖的路径“com.google.code.gson:gson:2.3”
更多关于Gradle配置介绍,可参考Gradle Project介绍
深入Task
自定义任务可以提高日常的开发效率,如自定义重命名apk名称的任务,处理版本号等,自定义任务可以在构建过程中的任何一步运行,非常强大
定义任务
Tasks 属于Project类,每一个task都实现 Task 接口.定义任务最简单的方式运行task方法,任务的名称作为参数
1 | task hello{ |
上面代码创建的hello任务,我们运行它会得到这样的输出
1 | $ ./gradlew hello |
初看可能会认为任务运行成功,但实际上“Hello ,world!”的输出是在任务运行之前。这个问题主要是因为gradel的task生命周期为 初始化 -> 运行配置 -> 运行任务 。task也对应三种语法:初始化语法,配置语法,任务指令语法;上面的任务其实是配置语法,即使我们运行其他的任务,“Hello,world!”也会输出
正确任务创建代码应该如下所示
1 | task hello << { |
上面的代码唯一的不同多了一个“<<”符号,这个符号表示这个任务执行的是运行任务语法而不是配置语法,为了比较两者的区别,可以参考如下代码
1 | task hello << { |
输出结果为
1 | $ ./gradlew hello |
因为Groovy有很多简写方式,在Gradlle中有几种定义task的方式
1 | task(hello) << { |
上面的代码第一种和第二种方式是一样的,我们可以加单引号也可以不加,圆括弧也是可选项,上面task的定义其实就是task方法接收两个参数,一个string的任务名参数,和一个闭包, task() 是Gradle Project 类中的方法
最后一个种实现不是通过task方法,而是通过tasks(TaskContainer的实例)对象的create方法,create方法接收一个map和闭包作为参数
剖析Task
Task接口是所有任务的基础接口,包含了一些通用的属性和方法,DefaultTask实现了这个接口,我们创建的的任务都继承于DefaultTask类
准确的来讲,DefaultTask不是真正的Task接口的实现类,Gradle内部有一个AbstractTask类实现了Task接口,但是AbstractTask是内部实现,我们不能继承重写,而DefaultTask继承自AbstractTask所以我们通过继承DefaultTask来创建任务
每一个Task包含了Action对象的集合,当一个任务执行时,所有这些action按顺序执行。给Task添加action,可以通过 doFirst() 和 doLast() 两个方法实现,这两个方法都接受一个闭包作为参数,然后传入Action对象中调用
在创建Task时,至少要实现doFirst和doLast中其中的一个,在先前我们的写法中,左位移符号(<<)其实是doFisrt方法的简写,如下为代码示例
1 | task hello { |
输出为:
1 | $ gradlew hello |
可以看到doFirst总是在任务的开始执行,doLast方法在任务的结尾执行,这意味着在使用这两个方法的时候要注意顺序,尤其是在顺序很重要的逻辑上。
如果task执行需要按顺序,我们可以使用 mustRunAfter() 方法,这个方法表示两个方法的执行的先后顺序关系,一个方法必须在另一个方法执行之后才能执行
1 | task task1 << { |
同时运行task1和task2会得到,不管命令中使用什么顺序,task2都在task1之后执行
1 | $ ./gradlew task2 task1 |
mustRunAfter() 方法没有添加依赖关系,也就是说执行只task2,task1不会执行,如果想使任务依赖另一个任务,使用 dependsOn() 方法
1 | task task1 << { |
输出为:
1 | $ gradlew task2 |
使用mustRunAfter方法,task1始终在task2之前执行,但是需要task1和task2都运行。使用dependsOn方法,即便只运行task2,因为task2依赖于task1,task1也会先执行后再执行task2。
使用Task简化Android打包流程
在Android中,当功能开发完毕,把apk发布到Android市场(Google Play等应用市场),需要对应用apk包进行签名,签名的配置如下:
1 | android { |
配置如上其实是很不安全的,一些安全信息如密码和key都写在了代码里,如果是上传到Git中的话,很轻易别人就拿到了这些信息。这里,可以通过自定义一个task每次打包之前询问密码,或者如果觉得这样比较繁琐,可以写在一个不被版本控制的文件中,如在项目根目录创建一个 private.properties 文件,然后在 .gitignore 文件中忽略它.
private.properties 文件中内容可以这么写
1 | release.password = thepassword |
我们现在定义一个 getReleasePassword 的任务
1 | task getReleasePassword << { |
这个方法的主要作用就是判断当前项目根目录中是否有 private.properties 文件存在,如果存在就load这个文件,找到key为 releas.password 的值;为了确保没有properties文件的用户也能运行,所以当找到不到properties文件时就在控制台询问用户输入.
1 | if (!password?.trim()) { |
关于上面部分的代码,首先是判断password是否为空, password?.trim() ,问号的作用是当password不为null时才调用trim方法,在groovy的if语句中字符的null或者空串都是false,所以不用单独判断
System.console().readPassword() 方法是groovy提供用来读取在控制台用户密码输入的方法,它返回的是一个字符数组,所以需要new String()去构造字符串
我们读取到密码后,就可以在gradle配置中对签名信息进行复制
这里假定keyPassword和storePassword是一致的
1 | android.signingConfigs.release.storePassword = password |
在Gradle打包过程中,只有在发布release包的时候才会做正式签名,所以这个任务需要依赖release任务,在 build.gradle 文件中添加如下代码:
1 | tasks.whenTaskAdded { theTask -> |
上面代码的主要意图是在Android的打包流程中在最后打apk时是有一个名为 packageRelease 任务实现,这个任务就是给apk加入签名信息,在执行这个任务之前必须要获取keystore的密码,所以这个任务的执行必须要依赖于先前自定义的 getReleasePassword 任务,这里不能直接调用packageRelease.dependsOn()去设置依赖,因为Android在打包过程中具体的打包任务其实是根据build variants动态生成的,所以在gradle构建build variant之前是没有packageRelease这个任务的,只有在每次build开始时去构建build variant才会有个packageRelease任务
执行 ./gradlew assembleRelease 命令会得到如下输出
从上面的截图可知,程序打包是没有找到private.properties文件,所以加了一些友好性的提示,去如何创建private.properties文件,然后再提示用户在控制台输入密码,从而完成打包
这个task的例子简单的介绍了如何在Android build流程中完成自定义任务,接下来的部分将详细介绍Android Gradle Plugin。
深入Android Gradle Plugin
在整个Android的开发过程中,大部分我们需要自定义的task都会跟Android插件(通过 apply plugin: ‘com.android.application’ 引入android插件)关联使用
使用Android Pugin中的构建流程需要合理使用build variants,使用起来非常简单,如下所示
1 | android.applicationVariants.all { variant -> |
applicationVariants 是所有variants的集合,通过迭代出每一个variant就可以获得对特定variant的应用,然后获得variant相应的属性,例如名称,描述等等;如果项目是一个Android Libraray那么applicationVariants应该改为librayVariants
注意到上部分代码在迭代集合内容时采用的是all()方法而不是原先介绍的each()方法,这是因为each只有在build variants创建之前触发,而all方法只要有新加入的variants就会被触发
这个技巧能够用来动态改变apk的名称,例如给apk名称加上版本号等,接下来的部分将详细介绍如何动态修改apk名称
自动重命名APK文件
在Android的打包流程中,最常见的需求就是通过给apk的名称加上版本号、渠道号重命名默认的Apk文件名称,具体实现可参考如下代码
1 | android.applicationVariants.all { variant -> |
从上面的代码片段可知,每一个build variant有一个outputs集合,Android App的outputs就是一个APK文件,output对象有一个属性叫outputFileName,通过修改outputFileName就可以修改最后apk的文件名。
结合Android插件hook的功能,我们还可以创建很多自动化的任务。 接下来,我们将学习如何为应用程序的每个build variant创建一个任务。
动态创建新Task
由于Gradle的工作原理和任务构建方式便利性,我们可以基于Android构建版本,在配置阶段轻松创建自己的任务。为了演示这个强大的功能,我们将学习到如何创建一个install任务,不仅仅是安装apk,而且安装之后再运行应用程序。 install 任务是Android插件的一部分,但是如果我们在命令行界面运行 gradlew installDebug 命令来安装应用程序,在安装完成后仍然需要手动启动应用。这一节将介绍如何创建install任务并自动打开应用首页。
首先来查看之前使用的 applicationVariants 属性:
1 | android.applicationVariants.all { variant -> |
对于每个build variant,我们需要检查它是否具有有效的install任务。因为正在创建的运行应用任务将依赖于install任务。一旦验证了安装任务存在,就会创建一个根据variant名称命名的新任务。这里需要使新任务依赖于variant的install任务,依赖设置的目的是为了在运行run任务之前先触发install任务。在tasks.create()方法中传递进来了一个闭包,闭包里面通过添加任务描述,当执行 gradlew tasks 时任务列表及其描述就会显示出来。
除了添加任务描述外,我们还需要添加实际的任务操作。在此示例中,需要启动应用程序。可以使用Android调试工具(ADB)在连接的设备或模拟器上启动应用:
1 | $ adb shell am start -n com.package.name/com.package.name.Activity |
Gradle有一个名为 exec() 的方法,可以执行命令行进程。为了使 exec() 工作,我们需要提供一个存在于PATH环境变量中的可执行文件,同时还需要使用 args 属性传递所有shell执行的参数,args接受一个字符串列表作为参数。 如下所示:
1 | doFirst { |
要获取完整应用包名,可以使用varaint的applicationId属性,如果不同的构建变体application id后缀不一致的话,这个属性也会包含后缀,这样就会产生一个问题,如下例子所示:
1 | android { |
程序包名为 com.gradleforandroid.debug ,但Activity的路径仍为 com.gradleforandroid.Activity 。为了确保Activity获得正确的加载路径,需要从applictionId中删除后缀:
1 | doFirst { |
上面代码中,首先,基于应用applicationId创建了一个名为classpath的变量。然后我们找到由 buildType.applicationIdSuffix 属性提供的后缀。 在Groovy中,可以使用减号运算符从另一个字符串中减去一个字符串。这些更改可以确保在安装apk后运行应用程序不会在使用后缀时失败。
创建自定义Gradle插件
如果我们有一个Gradle任务的集合,想在多个项目中进行复用,将这些任务提取到一个自定义插件轻松解决问题。不仅我们自己可以重用构建逻辑,还能分享给其他人使用。
插件可以用Groovy编写,也可以用其他使用JVM的语言,例如Java和Scala。事实上,Gradle的Android插件的大部分是用Java和Groovy组合编写的。
创建简单插件
要提取已存储在构建配置文件中的各种构建逻辑,可以在 build.gradle 文件中创建一个插件。这是开始创建自定义插件的最简单方法。
要创建插件,需要创建一个实现插件接口的新类。这里将使用在本章前面编写的代码,动态创建运行任务。插件类定义如下所示:
1 | class RunPlugin implements Plugin<Project> { |
Plugin 接口定义了一个 apply() 方法。Gradle在build.gradle中使用插件时调用此方法。 project 作为参数传递,以便插件可以配置项目或使用其中的方法和属性。在前面Task例子中,就不能直接调用Android插件中相关属性了,需要通过访问 project 对象来访问相应属性。请注意,我们访问Android插件的属性,需要在我们的自定义插件应用之前将Android插件在项目中apply。否则,可能会产生异常。
task的代码与之前相同,只有一个方法调用修改,通过调用project.exec()代替调用exec()。要确保在build.gradle文件中apply插件,将此行添加到build.gradle文件中:
1 | apply plugin: RunPlugin |
发布插件
为了发布一个插件并共享给其他人,我们需要将插件移动到独立模块(或项目)。独立插件具有自己的构建文件用以配置依赖项和发布方式。插件模块生会成一个JAR文件,包含插件类和属性。我们可以使用此JAR文件将插件应用于多个模块和项目,并与其他人共享。
与任何Gradle项目一样,需要先创建一个build.gradle文件以配置构建:
1 | apply plugin: 'groovy' |
编写独立的Gradle插件,首先需要应用Groovy插件。Groovy插件扩展了Java插件,使得能够构建和打包Groovy类。 Groovy和纯Java都是支持的,所以如果喜欢,我们可以混合使用它们。甚至可以使用Groovy扩展一个Java类,或者反过来也行。
构建配置文件中需要包含两个依赖关系:gradleApi()和localGroovy()。添加Gradle API来从自定义插件中访问Gradle相关基础接口,localGroovy()是Gradle安装附带的Groovy SDK的发行版。 为了方便起见,Gradle默认提供这些依赖项。 如果Gradle默认没有提供这些依赖,我们需要手动下载并引用它们。
如果我们计划以公开方式分发插件,需要确保在构建配置文件中指定组和版本信息,如下所示:
group =’com.gradleforandroid’
version =’1.0’
要开始使用独立插件模块中的代码,还要确保使用正确的目录结构:
1 | plugin |
和其他Gradle模块一样,需要提供一个src/main目录。因为这是一个Groovy项目,main的子目录称为groovy而不是java。还有一个resource的子目录,将使用它来指定插件的属性。
在包目录中创建一个名为 RunPlugin.groovy 的文件,在其中定义插件的类:
1 | package com.gradleforandroid |
为了让Gradle能够找到插件,需要创建一个属性文件,将此属性文件添加到 src/main/resources/META-INF/gradle-plugins/ 目录。文件的名称需要与我们的插件的id匹配。对于RunPlugin,该文件名为com.gradleforandroid.run.properties,文件内容如下:
1 | implementation-class=com.gradleforandroid.RunPlugin |
属性文件包含的唯一的东西是实现了Plugin接口类的包和名称。
当插件和属性文件准备就绪后,我们可以使用 gradlew assemble 命令打包插件。最终会在输出目录中创建一个JAR文件。如果我们还想把插件推送到Maven仓库,就还需要应用Maven插件:
1 | apply plugin: 'maven' |
接下来,配置uploadArchives任务,如下所示:
1 | uploadArchives { |
uploadArchives任务是预定义的任务。在任务中配置存储仓库后,就可以执行此任务来发布插件。这里就不详细介绍如何设置Maven存储库。
如果我们想让我们的插件公开,可以考虑发布到Gradleware的插件仓库。插件仓库有很多Gradle插件集合(不只是特定于Android开发)。 我们可以在的官方文档中找到有关如何发布插件的详细信息。
本文档不包括对自定义插件编写测试代码,但如果计划使插件公开可用,强烈建议进行代码的测试。我们可以在Gradle用户指南中找到有关编写插件测试的更多信息。
使用自定义插件
要使用插件,需要将插件添加为buildscript块的依赖项。 首先,配置一个新的依赖仓库。依赖仓库的配置取决于插件的分发方式。其次,就是需要在依赖关系块中配置插件的类路径。
这里如果包括我们在前面的例子中创本地生成的JAR文件,可以定义一个 flatDir 存储库:
1 | buildscript { |
如果将插件上传到Maven或Ivy仓库,配置会有不懂。 在第3章“管理依赖关系”中介绍了依赖关系管理,因此在这里就不再重复。
在设置依赖之后,就需要应用插件:
1 | apply plugin: com.gradleforandroid.RunPlugin |
使用apply()方法时,Gradle创建一个插件类的实例,并执行插件自己的apply()方法,我们就可以正常使用自定义插件了。
总结
在本章中,我们学习到了Groovy与Java的不同,以及如何在Gradle中使用Groovy,还看到了如何创建自定义的任务,以及如何hook任务到Android插件中。
在本章的最后一部分,还研究了如何创建插件,并确保可以通过创建一个独立的插件在多个项目中复用它们。 其实还有很多深入的知识不是本文档全部能覆盖的,更多的知识可以参考Gradle用户指南。