秒懂Gradle之从完全懵逼到是懂非懂

【版权申明】非商业目的注明出处可自由转载
博文地址: http://shusheng007.top/2020/06/13/01/
出自:shusheng007

概述

最近公司给Pluralsight 上开了账号,然后评测了一下自己Android相关的知识,发现gradle方面的知识比较欠缺,仔细想想也确实是,自己从来没有仔细研究过这个构建工具。刚好这两天有点时间,查阅了一下官方文档,争取从概念上理解这个构建工具,做到可以写build script,可以写gradle plugin 。

相信有很多做了几年Android开发的同学对于构建工具gradle仍然是一知半解的,甚至都不能完全理解那些build script,更别说修改了,每次要实现一个功能时都是Google一下,然后copy到自己的项目中,如果OK了就很开心,如果不OK就再搜一个,完全不能理解其中含义。这是最要命的,学习什么东西都应该从概念和原理上理解它,特别是当这个东西你已经接触很久了,更应该去理解其背后的设计。那样不仅记得牢,还会举一反三,进而在其基础上创新。

概述

本文确切的说是一篇读书笔记,阅读完本文后你会在脑中对gradle有个大概的理解,特别是使用过gradle的同学会有一种豁然开朗的感觉,你会突然发现你熟悉的那些build script 原来是这么个意思啊。。。这种醍醐灌顶的感觉是最好的。俗话说,授人以鱼不如授人以渔,在行文中我也会试图告诉你在遇到问题的解决思路。

本文包括的内容

  • gradle 概述
  • gradle 脚本基础
  • gradle 脚本实例

Gradle 概述

Gradle 是一个开源的自动构建工具,其几乎可以构建任何类型的软件,而不仅限于Java项目。

特点

Gradle官方宣称其具有如下特点:

  1. 高效 (High performance)
  2. 基于JVM ,其运行在JVM上 (JVM foundation)
  3. 遵守惯例(Conventions),其在Ant和Maven之后出现,使用了很多约定俗成的概念
  4. 易于扩展 (Extensibility)
  5. IDE支持(IDE support),例如 Android Studio, IntelliJ IDEA, Eclipse和NetBeans
  6. 可视化(Insight),通过Build scans 可以生成构建信息,易于调试和分析

俯瞰Gradle:

会当凌绝顶,一览众山小,就如《金字塔原理》描述的那样,看待一个事物应该先从全局着眼,然后再深入细节,才能做到心中有数。下面几点是我们必须要知道的:

  1. Gradle是一个通用的自动化构建工具,而不针对某个特定平台及语言

  2. 核心模式是基于Task的
    在这里插入图片描述
    如上图所示,其会构建一张Task的依赖图,整个构建过程就变成了一个task接着一个task的执行,直达完成的过程

    task是Gradle的最小执行单元,由如下3部分构成:

    • 输入:值,文件等
    • 动作:执行的动作
    • 输出:值,文件等

    前一个task的输出是后一个task的输入

  3. gradle分3个阶段顺序执行

    • 初始化阶段: 设置构建环境
    • 配置阶段 : 确定任务执行序列, 此阶段每次 build run 都会执行
    • 执行阶段: 按照配置执行
  4. . 支持多种扩展方式

    • 自定义Task类型
    • 自定义Task动作 Task.doFirst() and Task.doLast() methods.
    • 在project 与 task 使用 Extra properties
  5. 构建脚本基于API调用,而非基于配置
    你可以将构建脚本当代码阅读,而不是配置文件。它只会告诉你如何一步一步的将软件构建出来,而不会告诉你每一步是如何做的。这也是与我们日常工作最密切的部分。

阅读完以上的5点,你应该对gradle有一个模糊的概念了,接下来才是本文的重点,也是与日常开发息息相关的:gradle script

Gradle 脚本

Gradle script 是用来指导Gradle 如何构建项目的,一般由Groovy DSL 编写,现在也支持Kotlin DSL 编写。

以前我天真的以为我不会写gradle 脚本是因为我不懂Groovy语言,这下好了,gradle支持了Kotlin我应该会写了。后来发现完全不是那回事,人家使用的是领域特定语言,英文为domain-specific language (DSL) ,与我们平时使用的 general-purpose language (GPL) 完全不是一回事,关键是它还有自己的语法和API,所以使用Kotlin和Groovy基本一毛一样。

让我们来感受一下使用两种语言定义一个gradle的task

Groovy:

task hello{
    doLast{ println "hello world!" }
}

Kotlin

task ("hello"){
    doLast{ println "hello world!" }
}

上面两种写法几乎是相同的,所以别再骗自己了,让我们去懂它吧。

脚本基础对象

理解 gradle 最关键的是要理解 projecttask,这两个概念是构成gradle script的基石。

  • project
    这个project是指gradle的project,并非我们项目。具体代表什么和我们要构建的软件类型相关,例如我们建立了一个多module的Android 应用,各个module就各是一个gradle project, 他们一起构成了一个gradle build。project比较抽象,需要慢慢理解。
  • task
    task 代表构建过程中的不可分割的执行任务。
    例如编译一下classes,创建一个JAR,产生Javadoc 等等,都可以是一个task。每个project又包含一个或者多个task。

依照IT世界的惯例,先写一个hello world出来

  1. 安装gradle
    如果是window系统,下载zip包并解压,然后配置环境变量即可
  2. 创建一个文本文件,命名为build.gradle
  3. 在此文件中创建一个名为hello的task
    task hello{
        doLast{ println "hello world!" }
    }
  4. 执行task
    导航到build.gradle 文件目录下,在命令行中执行

    gradle hello
  5. 查看输出

    > Task :hello
    hello world!
    
    BUILD SUCCESSFUL in 1s
    1 actionable task: 1 executed

    是不是很简单啊

上面的脚本创建了一个叫hello的task,然后输出一句hello world

其中task是gradle的Api, 其是Project 接口的一个方法,签名如下

//Creates a {@link Task} with the given name and adds it to this project.
Task task(String name) throws InvalidUserDataException;

注意,那个project是gradle的一个接口,这个可厉害了,我们都是通过它来使用gradle的功能的。签名如下:

//This interface is the main API you use to interact with Gradle from your build file. From a <code>Project</code>,
//you have programmatic access to all of Gradle's features.
public interface Project extends Comparable<Project>, ExtensionAware, PluginAware {
    ...
}

doLast 是Task接口里面的一个方法,在一个task体执行完其他任务后执行。

doLast 源码:

    @Override
    public Task doLast(final Closure action) {
        hasCustomActions = true;
        if (action == null) {
            throw new InvalidUserDataException("Action must not be null!");
        }
        taskMutator.mutate("Task.doLast(Closure)", new Runnable() {
            @Override
            public void run() {
                getTaskActions().add(convertClosureToAction(action, "doLast {} action"));
            }
        });
        return this;
    }

看到这些源码是不是感觉很亲切啊,为什么到了gradle script中就看不懂了呢?嗯,那是因为gradle 脚本语法与传统的groovy,kotlin 语法不一样,其DSL,有自己的语法。

接下来,我们就简单的介绍一下gradle script,达到可以满足日常开发需求,不至于一遇到gradle 脚本就两眼一抹黑,修改全靠搜,不行重复搜。

脚本基础语法

要想读懂gradle script,首先就需要熟悉其语法构成元素:

语法元素

  1. Project : build script 的隐含对象

    通过它来使用gradle的功能,例如下面我们熟悉的块都是project对象的方法,只不过满足一定的方法签名,可以写成block的形式,这个我们后面会细说。

    buildscript{
        ...
    }
    configurations {
        ...
    }
  2. 属性
    能使用=$ (模板符号)的都是属性

    version = '1.0.1'
    myCopyTask.description = 'Copies some files'
    
    file("$buildDir/classes")
    println "Destination: ${myCopyTask.destinationDir}"

    上面的代码中,version和buildDir都是project的属性,description和destinationDir 是myCopyTask 这个Task的属性

  3. 方法
    ()的都是方法,但是groovy 支持无()调用,所以无括号时就看后面有没有=号,没有就是方法。

    ext.resourceSpec = copySpec()   // `copySpec()` comes from `Project`    
    file('src/main/java')
    println 'Hello, World!'

    其中println 也是方法,这里的方法一般都是实例方法,就是说其是某个实例的方法。

  4. Block
    一种特殊的方法,最有用也最具有迷惑性,其表现如下

    <obj>.<name> {
         ...
    }
    
    <obj>.<name>(<arg>, <arg>) {
         ...
    }

    只有符合如下签名的方法才可以使用block语法

    • 至少有一个方法入参
    • 方法的最后一个入参类型必须是groovy.lang.Closure 或者 org.gradle.api.Action.

    例如下面这个输入project的copy方法

    copy {
        into "$buildDir/tmp"
        from 'custom-resources'
    }

    它为什么可以写成block的形式呢,让我们看一下它的签名。它有两个版本,一个的入参是groovy.lang.Closure 类型,一个对应的版本入参是org.gradle.api.Action。Closure 版本是为了向前兼容的,新加的API都是使用Action版本。

    //Closure 版本
     / * Copies the specified files.  The given closure is used to configure a {@link CopySpec}, which is then used to copy the files*/
    WorkResult copy(Closure closure);
    
    //Action版本
    WorkResult copy(Action<? super CopySpec> action);

    可以看到,其签名是符合block语法要求的,我们可以发现 into 和 from也是两个方法,但是这两个方法是属于哪个类型的呢?是project吗?
    通过查询确认不是。其实这两个方法是属于 CopySpec 类型的。

    对于如何得知 block里面的方法属于哪个类型,就需要分情况了。对于 Closure 版本需要看注释,或者文档。对于Action版本看泛型类型就行,gradle新增的API 都使用 Action的方式,即使是老的API也添加了对应的Action版本。

Task 依赖

task hello {
    doLast {
        println 'Hello world!'
    }
}
task intro {
    dependsOn hello
    doLast {
        println "I'm Gradle"
    }
}

通过dependsOn,intro task 依赖了hello,当我们执行into Task时,hello task也就被执行了。

Extra task properties

这个用的很多,Extra 就相当于一个Map,我们可以把值按key-value的形式放在里面,用的时候通过key来取。

task myTask {
    ext.myProperty = "myValue"
}

task printTaskProperties {
    doLast {
        println myTask.myProperty
    }
}

上面代码在myTask中给其ext 放入了 myProperty- myValue ,然后在其他task中就可以通过key 获取到那个value了。 groovy代码表现的不明显,让我们看下kotlin的。

tasks.register("myTask") {
    extra["myProperty"] = "myValue"
}

tasks.register("printTaskProperties") {
    doLast {
        println(tasks["myTask"].extra["myProperty"])
    }
}

是不是清晰多了,先找到myTask 然后从其extr中使用myProperty 做为可以获取其value。

使用方法和Ant

task loadfile2 {
    doLast {
        fileList('./antLoadfileResources').each { File file ->
            ant.loadfile(srcFile: file, property: file.name)
            println " *** $file.name ***"
            println "${ant.properties[file.name]}"
        }
    }
}

File[] fileList(String dir) {
    file(dir).listFiles({file -> file.isFile() } as FileFilter).sort()
}

和其他脚本语言一样,我们可以把常用的功能分装成method,然后在Task中调用,方法的定义就非常接近Java了。

上面的方法fileList()作用是将某个目录下的文件过滤出来并按名称排序。在loadfile2 task 中使用ant task 读取文件内容并展示。

有的同学又懵了,日了个狗,Ant又是什么东西? 为什么说基础扎实,经验丰富的程序员学啥都快呢?因为一个新的东西不会凭空出现,往往与既有的体系有千丝万缕的联系,人类之所以可以不断进步,就是因为我们的知识可以一代一代的传承。 简单来说,Ant 与gradle是竞品,Ant是gradle的前辈,gradle和maven是站在Ant的肩膀上发展起来的。所以gradle也会吸收Ant很多优秀的东西,包括思想和实现。

在build script 中添加外部依赖

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath group: 'commons-codec', name: 'commons-codec', version: '1.2'
    }
}

这个在android开发中已经见的很多了,不再赘述。

总结

行文到此就要结束了,如果你认真阅读了以上内容,相信你对gradle script会有新的认识,特别是block语法,当你阅读gradle 脚本的时候心中再也不慌了,虽然你可能不知道具体的含义,但是你清楚的知道他们背后是什么,如何去查询,这就是本文的目的。

想要更进一步,当然是去看官方网站 官网

如果有什么问题可以在留言区留言讨论。由于本人一向思维缜密,bug极少… Hi,施主你别喷我啊!施主,你别拍。。。施主。。。 我r, gsn。。。fighting

About the Author: shusheng007

发表评论

邮箱地址不会被公开。