gradle的基础知识和自定义plugin

本文主要从这几个方面来介绍gradle。 希望看完这篇文章能够了解gradle是什么,以及如何使用它。

一. 为什么要学习gradle

gradle是基于Apache AntApache Maven概念的项目自动化构建工具。并且基于groovy的特定领域语言(DSL)来编写脚本。groovy语言极其类似JAVA语言,且更简于JAVA,能够让使用者更加灵活的自定义构建配置。利用gradle可以实现自动化执行和管理构建流程。

二. gradle 生命周期

因为gradle是一种基于依赖的构建语言,也就是说需要在执行tasks阶段前,确定所有参与构建中的project,以及对定义的tasks以及其依赖的tasks能够形成一个正确的有向无环图。因而gradle在构建过程中,有三个不同的时期,分别为:初始化阶段,配置阶段,执行阶段。

initialization: 初始化阶段

主要工作:解析整个工程中所有的project,构建所有的Project对应的project对象。其中文件settings.gradle就是在初始化阶段执行的,这个文件主要是用来决定哪些项目(project)参与到构建中。当一个项目越来越多时,往往会添加多个library-module,对于每一个module-libraray的添加,我们都需要在settings.gradle文件上声明,否在对于整个项目来说,添加的module,只是一个包含了一些代码源文件的文件夹,并不能参与到整个项目的构建中来。例如:

include ':app',':module_base',':module_home',':module_h5'


configuration: 配置阶段

​ 主要工作:解析所有的project对象中的task,构建好所有tasks的拓扑图,也就是有向无环图,在配置阶段,会执行到文件build.gradle. 并下载所有以apply plugin声明的所有插件。

execution: 执行阶段
执行具体的task和其依赖的task.

三. gradle project

随着app模块的逐渐增多,一个android项目通过组件化的方式,可以让各个模块分工明确,同时也可以让开发专注于自己的模块开发。那么gradle如何管理这些模块呢?在gradle里面,一个模块可以认为是一个project.

projectbuild.gradle文件之间是1对1的关系,具体一个项目有多少个project, 是在构建生命周期中的initialization阶段,查找settings.gradle来确定的。

一个project中又可以包含多个task,我们可以自定义task来处理一段逻辑。至于如何创建task在接下来一节会介绍。

一个project同时会有多个依赖,对于android项目,依赖来自于这三个部分:Library modules,aar modules,jar libraray. 对这三种依赖调用的方式也是不一样的。这点在project具体的api会介绍。

一个project有一个名字,和一个唯一有效的路径来唯一确定它。同时一个project可以有一些插件(plugin), 如android的lib模块,我们就可以使用:

apply plugin: 'com.android.library'

同时projectgradle作为一个类存在的话,具有很多属性。在构建脚本中,我们同样可以设置想要的属性和扩展需要的属性。比如通过ext来定义。

接下来就介绍一些常用的project的api

3.1 project api

allprojects

这个属性是包含该项目及其它的子项目。我们可以在其包含的块中添加每个project都需要配置的东西,这样就可以避免重复配置和方便修改。

在顶级build.gradle文件中加入这段代码后:

allprojects {
    println "the project name is :: " + project.name
}

输出为:

> Configure project : 
the project name is :: GradleDemo
the project name is :: app

可以看出这段代码在配置阶段的时候,就会被调用,同时会输出root project 和它的子project.

buildscript

管理编译这个项目的仓库和依赖。通常在android项目中,设置在顶级的build.gradle中,用来定义项目中所有模块公用的gradle存储区和依赖项。 在buildscript中,也有一个属性dependencies,这里的dependencies定义的是gradle构建工具所依赖的库,而非是project依赖的库。

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.2.1'
    }
}

ext

通过在ext命名空间来声明属性实现扩展属性的功能。

顶层build.gradle

ext {
    test = "hello world"
}

app下的build.gradle

task hello {
    doLast {
        println parent.ext.test
    }
}

在终端执行:

./gradlew -q hello

可以正确得到hello world的输出。

dependencies:

project的三方依赖,上面也说过,android项目的依赖主要来自三种:库依赖,aar依赖,jar依赖,按照来源可分为外部依赖,文件依赖,和项目以来。对于三种依赖的写法稍有不同,

implementation fileTree(include:['*.jar'],dir:'libs')  //文件依赖
implementation 'commons-lang:commons-lang:2.6' // 外部依赖
implementation project(path:':sdk') //项目依赖

这里要强调一下在声明依赖的时候,对于库的范围限制的关键字。首先是apiimplementation

看一下官方文档的解释:

The apiconfiguration should be used to declare dependencies which are exported by the library API, whereas the implementationconfiguration should be used to declare dependencies which are internal to the component.

Dependencies appearing in the api configurations will be transitively exposed to consumers of the library, and as such will appear on the compile classpath of consumers. Dependencies found in the implementation configuration will, on the other hand, not be exposed to consumers, and therefore not leak into the consumers' compile classpath.

也就是说呢:api的依赖会传递给消费者,而implementation的依赖只对本身可见。举个例子说:

case 1: 自定义了库A,A api 依赖了B,那么当消费者新建了工程,引用了库A时,该消费者依然可以使用库B的api。

case 2: 自定义了库A,A implementation 依赖了B,那么当消费者新建了工程,引用了库A时,想用B的api,只有重新创建对B的依赖。

这样万一库A升级了,不再依赖B,那么消费者的项目就不会崩溃。

用一个官方文档上的java代码,就恨容易看清楚了。

// The following types can appear anywhere in the code
// but say nothing about API or implementation usage
import org.apache.commons.httpclient.*;
import org.apache.commons.httpclient.methods.*;
import org.apache.commons.lang3.exception.ExceptionUtils;
import java.io.IOException;
import java.io.UnsupportedEncodingException;

public class HttpClientWrapper {

    private final HttpClient client; // private member: implementation details

    // HttpClient is used as a parameter of a public method
    // so "leaks" into the public API of this component
    public HttpClientWrapper(HttpClient client) {
        this.client = client;
    }

    // public methods belongs to your API
    public byte[] doRawGet(String url) {
        GetMethod method = new GetMethod(url);
        try {
            int statusCode = doGet(method);
            return method.getResponseBody();

        } catch (Exception e) {
            ExceptionUtils.rethrow(e); // this dependency is internal only
        } finally {
            method.releaseConnection();
        }
        return null;
    }

    // GetMethod is used in a private method, so doesn't belong to the API
    private int doGet(GetMethod method) throws Exception {
        int statusCode = client.executeMethod(method);
        if (statusCode != HttpStatus.SC_OK) {
            System.err.println("Method failed: " + method.getStatusLine());
        }
        return statusCode;
    }
}

dependencies {
    api 'commons-httpclient:commons-httpclient:3.1'
    implementation 'org.apache.commons:commons-lang3:3.5'
}

如果有一个lib模块下有这个类,那么由于开放了public的构造函数,这个构造函数就会被消费者调用,那么httpClient就需要暴露给消费者。为了保证库的版本一致性,这个时候用api的依赖方式会更好。而在doGet方法中,由于是private,只对类内有效,所以只需要用implementation 的依赖方式即可。

Configuration name Description
api This is where you should declare dependencies which are transitively exported to consumers, for compile.
implementation This is where you should declare dependencies which are purely internal and not meant to be exposed to consumers.
compileOnly This is where you should declare dependencies which are only required at compile time, but should not leak into the runtime. This typically includes dependencies which are shaded when found at runtime.
runtimeOnly This is where you should declare dependencies which are only required at runtime, and not at compile time.

四. gradle task

项目和build.gradle 文件一一对应,但build.gradle中可以有任意个task. 而task相当于java类中的一个方法,可以处理一段逻辑。

task 的创建

task hello {
    println "hello world 1"
}

task(hello2) {
    println "hello world 2"
}

task("hello3") {
    group = 'hello'
    doFirst {
        println "hello world3"
    }
}

tasks.create(name:"hello4",group:"hello") {
    doLast {
        println "hello world4"
    }
}

task 的依赖

hello3.mustRunAfter hello4

hello4.dependsOn hello2

执行命令./gradlew -q hello3 hello4

hello world 2
hello world4
hello world3

通过mustRunAfterdependsOn就可以保证hello4 这个task执行在hello2hello3之间。

hello3.finalizedBy hello4

意思是hello3执行完之后,必须要执行hello4。 比如我们的集成测试task就需要在单元测试task执行之后才能执行。

执行命令./gradlew -q hello3

输出结果:

hello world3
hello world4

task 的输入输出

除了上述的依赖关系来连接两个task外,还可以通过taskinputoutput来关联两个task. 另外gradle通过与上一次执行task的输入输出,如果没有发生变化,那么就不会重复执行该task, 并且在终端上也会提示UP-TO-DATE

我们定义一个任务writeTask,执行的逻辑就是将版本信息写入到releases.xml中,然后在每次clean这个task之后执行它。然后我们执行两次clean .

ext {
    versionName='1.0.0'
    versionCode='100'
    versionInfo='app 的第一个版本,上线了一些基础核心的功能。'
    destFile = file('releases.xml')
    if (destFile != null && !destFile.exists()) {
        destFile.createNewFile()
    }
}


task writeTask {
    inputs.property("versionCode", this.versionCode)
    inputs.property("versionName", this.versionName)
    inputs.property("versionInfo", this.versionInfo)

    outputs.file destFile

    doLast {
        def data = inputs.getProperties()
        destFile.withWriter('utf-8') {
            writer -> writer.writeLine data.toMapString()
        }
    }
}

tasks.getByName("clean").finalizedBy(writeTask)

第一次输入:./gradlew clean

> Task :app:writeTask 
Putting task artifact state for task ':app:writeTask' into context took 0.0 secs.
Up-to-date check for task ':app:writeTask' took 0.002 secs. It is not up-to-date because:
  Task ':app:writeTask' has additional actions that have changed

:app:writeTask (Thread[Task worker for ':',5,main]) completed. Took 0.013 secs.

第二次输入./gradlew clean

> Task :app:writeTask UP-TO-DATE
Putting task artifact state for task ':app:writeTask' into context took 0.0 secs.
Skipping task ':app:writeTask' as it is up-to-date (took 0.0 secs).

:app:writeTask (Thread[Task worker for ':',5,main]) completed. Took 0.001 secs.

第二次执行的时候会提示UP-TO-DATE,提示已经是最新的了。

五. gradle plugin

定义gradle插件主要有三种方式:

  1. 脚本

  2. 继承plugin这个接口的自定义类

  3. 远程仓库的jar包

所以对这三种方式的脚本的引入也是有区别的。这里呢,一一举例说明。
第一种
比较好理解。 通过新建gradle文件,然后利用apply from 即可引入。比如,我想将我发包的脚本写成一个插件,引入进来。

apply plugin: 'maven'

afterEvaluate { project ->
    uploadArchives {
        repositories {
            mavenDeployer {
                pom.groupId = "aa"
                pom.artifactId = "bb"
                pom.version = "cc"
                repository(url: "http://www.baidu.com") {
                    authentication(userName: "admin", password: "123")
                }
                snapshotRepository(url: "http://www.baidu.com") {
                    authentication(userName: "admin", password: "13")
                }
            }
        }
    }
}

然后我在我的app模块下的build.gradle引入这个plugin就可以了
apply from:rootProject.file("release.gradle")
第二种

GreetingPlugin.groovy新建类GreetingPlugin实现接口Plugin<Project>,在方法apply中定义需要执行的任务。文件目录结构如下:

tmp7ffbe9a9.png

import org.gradle.api.Plugin
import org.gradle.api.Project

class GreetingPlugin implements Plugin<Project> {
    @Override
    void apply(Project project) {
        project.task('sayHello') {
            doLast {
                println 'Hello from the GreetingPlugin'
            }
        }
    }
}

在文件GreetingPlugin.properties中添加如下代码

implementation-class=GreetingPlugin

app 模块下的build.gradle文件下添加如下代码即可。

apply plugin:GreetingPlugin

输入命令./gradlew -q sayHello
输出:

Hello from the GreetingPlugin

第三种
先上传到maven.

apply plugin: 'groovy'
apply plugin: 'maven'

dependencies {
    compile gradleApi()
    compile localGroovy()
}

repositories {
    mavenCentral()
}

group='org.gradle:customPlugin'
version='1.0-SNAPSHOT'
uploadArchives 
    repositories {
        mavenDeployer {
            repository(url: uri('../repo'))
        }
    }
}

然后在buildscript块下写上依赖的classpath即可。

buildscript {
    repositories {
        maven {
            url = uri(repoLocation)
        }
    }
    dependencies {
        classpath 'org.gradle:customPlugin:1.0-SNAPSHOT'
    }
}
apply plugin: 'GreetingPlugin'

这样呢,gradle 差不多就介绍完了,还有一些其他的模块,比如sourceSet,buildType这些各位可以自己查看文档,应该很容易理解了。

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

推荐阅读更多精彩内容