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