Gradle学习记录(一)----Gradle开发团队入门教程

714 阅读11分钟

参考资料:
来自Gradle开发团队的Gradle入门教程 1.Gradle与Groovy基础 2.Gradle构建 3.插件编写入门

image.png

Gradle 基础

Gradle基础概念

gradle是一个通用的构建工具 gradle是纯java编写的,核心代码都是java的,也是运行在jvm上。 和绝大多数运行在jvm上的库一样,gradle本身的安装包就是一个脚本和一堆java的lib的集合。

gradle的源码在github上都有【github】

【下载gradle】

gradle网站上的三种包:

gradle-7.4.2-src.zip —— sources包:某个版本的源代码打包,不可以运行。

gradle-7.4.2-bin.zip —— bin包:可以运行,且不包含多余的东西

gradle-7.4.2-all.zip —— all:可以运行,但是包含一些例子,sample和文档

gradle的本质

基于jvm的程序本质就是一个bin脚本,这个bin脚本分为unix上的和windows上的,做的事情就是启动jvm 加载lib目录下的运行所需要的库。(maven和groovy也是相似的结构)

image.png

lib目录中可以看到都是jar包

image.png

wrapper介绍

我们先来看下执行下面这条命令实际上会做些什么吧

./gradlew help

./gradlew 是为了保证在运行gradle的时候使用设定好的gradle版本,如果没有,则会下载。 执行 ./gradlew本质是启动一个很小的jvm,这个jvm去加载gradle-wrapper.jar,然后去把真正的安装包下载下来。接下来在这个指定的这个gradle版本中去执行对应的命令。

image.png

gradle-wrapper.properties文件介绍

这个配置文件主要负责提供gradle远程下载地址和提供本地保存路径

#本机 Gradle 压缩包解压后主地址
distributionBase=GRADLE_USER_HOME

#Gradle 压缩包下载地址
distributionUrl=https://services.gradle.org/distributions/gradle-7.0.2-bin.zip

#本机 Gradle 压缩包解压后路径  主要在这里找
distributionPath=wrapper/dists

#本机存放 Gradle 压缩包主路径
zipStorePath=wrapper/dists

#本机存放 Gradle 压缩包主地址
zipStoreBase=GRADLE_USER_HOME

GRADLE_USER_HOME

~/.gradle

image.png

gradle在机器上运行时只和两个目录打交道,一个是你的项目目录,另外一个就是gradleuserhome。因为执行的时候就是去调用不同jar包中的方法。

Daemon模式介绍

jvm的启动很慢,需要load很多jar包。gradle3.0之后默认使用daemon模式,当我们执行gradle命令时,例如gradle compile 命令时,会先启动一个很小的jvm —— client jvm。这个jvm基本什么事情都不做,只负责连接查找,并和后台的一个叫做 daemon 的jvm 通信,client jvm 会把执行参数和任务发送给daemon,daemon执行完任务后再把参数发挥给client jvm,所以client只负责发送接收数据和任务请求。

连接结束时,daemon会一直存在,client会销毁。

可以搜索一下gradle daemon看下文档。 Daemon一般空闲超过三个小时就会自动销毁。 可以使用--no -daemon参数来控制daemon,现在daemon已经很稳定了,一般不需要设置这个参数。

每一个./gradlew 都和一个gradle版本绑定。

示例:

    1. ./gradlew compileJava
    1. 会启动一个很轻量的jvm来查找机器上有没有对应版本的gradle,安装了的话就执行下一步,否则会先去下载对应版本的gradle
    1. 查找版本为5.0(假设)的并且和当前构建所要求的相关参数所兼容的daemon。若没有找到,就会启动一个daemon,并通过socket和这个daemon连接。

命令: ./gradlew --stop 杀死所有的daemon

image.png

groovy基础

《groovy in aciton》 看这本书就够了, groovy是运行在jvm之上的脚本语言,所谓脚本语言,指的是groovy是强类型,但是是动态调用的。

快速执行groovy代码

这个很重要,可以在这里点进去groovy对应的类 image.png

List list = [1,2,3,4,5]
list.findAll{it % 2 != 0}
println(list)

groovy和java很像,但默认都是public

gradle脚本一般使用groovy语言写代码

groovy有很多语法糖,groovy也是可以用java的方式去写


//List l = []
//Map m = [a:1]
//
//class A{
//    int a
//
//    void printA(){
//        println(a)
//    }
//}
//
////会生成一个默认的构造函数 new A(a:3)是简写,本质是一个map,new A([a:3])
//new A(a:3).printA()
//看起来是一个静态调用
////正常的java调用是通过 invokeDynamic 去调用printA方法
////但是groovy的运行方法可以想象为纯反射调用,等效为先生成一个对象A,再通过反射调用
//def a = new A(a:5)
//invokeMethod(a,"printA",[])//最后的[]表示没有参数

//闭包{}  是groovy dsl的核心
//闭包可以理解为就是一个函数,没有return 默认返回最后一行,可以传递进去参数,不指定入参,会有隐式入参it

def closure = {return 1}
println(closure())//打印 1    这里注意这个closure是带()的,因为加括号才表示这个闭包的执行,才有返回值

def closure2 = {param ->
    param+2
}
println(closure2(4))//打印6

def closure3 = {
    it + 5
}
println(closure3(5))//打印10

//可以再
List list = [1,2,3,4,5]
list.findAll{it % 2 != 0}
println(list)

//groovy.lang.Closure
def test22(int i , Closure c){//这个不需要导入  记下来
    return c(i)
}
//如果最后一个参数是闭包,可以这么写代码
println test22(3,{it * 3})//9
//groovy约定不影响歧义的情况下是可以不加括号的  重点
//上方的代码等同于 println(test22(3,{it * 3}))
println(test22(3,{it * 3}))//9
//闭包是最后一个参数的话,闭包可以写在外面
println test22(4){
    it +2
}//6

---结束

DSL: 领域专属语言

搜索groovy dsl,学习groovy ,必看 docs.groovy-lang.org/docs/latest…

关于闭包的简单介绍

plugins {
    id "com.diffplug.spotless" version "5.2.0"
}
//相当于有一个方法名为plugin的方法,我们调用这个方法,并且入参是一个闭包
plugins({
    id "com.diffplug.spotless" version "5.2.0"
})
//闭包内可以转化为方法的调用,先调用id方法,用id方法返回的对象调用version方法
plugins({
    id("com.diffplug.spotless").version("5.2.0")
})

grovvy重点理解的就是两点:一个是闭包,一个是动态调用

核心api一般会在 org.gradle.api.Porjet类中,比如可以在这里搜索buildscript方法

image.png

org.gradle.api.Porjet
void buildscript(Closure configureClosure);

org.gradle.api.Porjet
void repositories(Closure configureClosure);


这里的buildscript其实就是调用Project中的buildscript方法
可以在github中gradle的项目中看源码 org.gradle.api.Projcet

2.2 gradle 的构建

image.png

gradle lifecycle 生命周期

Gradle 构建具有三个不同的阶段。

  • 初始化

    Gradle 支持单项目和多项目构建。在初始化阶段,Gradle 确定哪些项目将参与构建,并为每个项目创建一个Project实例。

  • 配置

    在此阶段配置项目对象。执行作为构建一部分的所有项目的构建脚本。(指的是我不运行这个构建,只是进行一些配置,相当于从头到尾执行这个gradle文件)(可以理解为对一个东西进行配置,对于构建来说,你要对一个构建进行配置,意识是说这个构建中有哪些任务,它运用了哪些插件,它有哪些项目,它可以执行哪些逻辑)
    配置阶段会把整个gradle从上到下的执行一遍,所谓的执行就是运行gradle文件中的groovy代码,就是把build.gradle当成groovy代码执行一边

  • 执行

    Gradle 确定在配置阶段创建和配置的要执行的任务子集。该子集由传递给gradle命令和当前目录的任务名称参数确定。Gradle 然后执行每个选定的任务。

gradle中执行的最小单元是任务(task

下方就属于一段非常简单的**构建脚本 **
image.png

调用的方法是,第一参数是task的名字,第二是具体执行的配置 Task task(String var1, Closure var2);

```
task("hello1",{
    println 'lalalala----------------'
    doLast {
        println "doLast------------------"
    }
})
```

配置阶段互创建一个新的名为hello的任务,使用
{
    println 'lalalala----------------'
    doLast {
        println "doLast------------------"
    }
}
    这个函数去configure刚刚创建的hello这个task,就是groovy delegate机制(TODO 可以在官网搜索groovy delegate )
    
gradle DSL允许在不引起歧义的情况下,把括号省略掉

task("hello1") {
    println 'lalalala----------------'
    doLast {
        println "doLast------------------"
    }
}
    

Task.java ,可以搜索到doLast,含义是把dolast中的这个函数放到这个任务的执行动作列表的末尾,但不真正的执行它,因此在同步代码的时候,doLast------------------不会被打印出来,只有执行hello任务的时候,才会打印。下方是Task中doLast方法官方注释

/**
 * <p>Adds the given closure to the end of this task's action list.  The closure is passed this task as a parameter
 * when executed.</p>
 *
 * @param action The action closure to execute.
 * @return This task.
 */
Task doLast(Closure action);

想要执行doLast中的代码需要执行hello1这个任务
./gradlew hello1

> Configure project :
lalalala----------------

> Task :hello1
doLast------------------

可以看到配置阶段(Configure project)只答应了lalala,任务的执行阶段才打印doLast

Gradle核心模型

gradle的本质就是把lifecycle和一堆api结合起来

Project

Project

一个Projecet就代表一个项目在jvm中的一个实例,非常重要,因为在build.gradle中一切无主的方法(无主的方法),都会去Project上去查找

task 的api也是在Project类中。
projet是一个树状的,可以通过api来查找父project和子project

project.parent.childProjects
getProject().getParent().getChildProjects()

这两行代码是一致的,第一行代码点进去后发现实际调用的就是第二行的代码
get方法中的get是可以省略的

Task 任务

任务之间可以依赖
定义好的任务同步后可以在右侧gradle目录中看到

image.png
task定义时,名字可以不存在,可以动态创建task,比如在for循环中创建任务

image.png

${name}调用了Task实例中的getName方法。

执行任务可以选择双击右侧gradle中的task名称,也可以选择在下方的terminal中执行命令

image.png groovy中有更方便的循环语句

10.times{i ->
    task("supermanman_"+i){
        def temp = i;
        doLast{
            println("Executing task ${temp}")
        }
    }
}

Task 方法介绍

dependsOn

task('fc'){
    doLast{
        println("i am first task")
    }
}

10.times{i ->
    task("supermanman_"+i){
        dependsOn('fc')
        def temp = i
        doLast{
            println("Executing task ${temp}")
        }
    }
}

执行 ./gradlew supermanman_1  
输出

> Task :fc
i am first task

> Task :supermanman_1
Executing task 1

doFirst/doLast 只有在任务列表中真正执行了对应的task,才会执行

task('fc'){
    println("执行在configuring 阶段")

    doLast{
        println("i am first task  执行在Execution阶段")
    }
}

Lifecycle 与Hook

看如何调用钩子函数,我们之前说过如果遇到无主的函数,就先去看是不是Project类中的方法。Cofiguration阶段本质上就是把build.gradle文件从上到下跑一遍,这时候就称为这个project已经完成evaluate【评估】

afterEvaluate就是钩子函数,参数是一个闭包,执行到这的时候会把这个闭包放到执行列表的最后。表示gradle的执行引擎把build.gradle这个代码脚本从上到下跑一遍,在去执行这个钩子函数中的闭包,闭包就可以看做是函数。

gradle生命周期中的三个阶段中,初始化阶段我们一般不需要关心


afterEvaluate {
    println("after evaluate")
}

task('fc'){
    println("执行在configuring 阶段")

    doLast{
        println("i am first task  执行在Execution阶段")
    }
}

10.times{i ->
    task("supermanman_"+i){
        dependsOn('fc')
        def temp = i
        doLast{
            println("Executing task ${temp}")
        }
    }
}
命令 ./gradlew supermanman_1

输出 
> Configure project :

执行在configuring 阶段
after evaluate

> Task :fc
i am first task  执行在Execution阶段

> Task :supermanman_1
Executing task 1

执行./gradlew supermanman_1的时候还没有这个任务,gradle这时也不关心是否有这个任务。这时候会先进入到配置阶段,配置阶段结束后再去执行具体的task,因为我们在配置阶段的时候会动态的去生成这个task,所以执行阶段的时候就可以找得到


插件编写

所谓的插件,就是一定的业务逻辑的打包的集合

插件的API是Pluglin

/**
 * 这个类不需要和执行的代码放在同一个单元中,也就是和apply 可以放在不同的文件中
 * 声明完这个类之后,不需要创建这个类的实例,什么也不需要做
 * */
class MyPlugin implements Plugin<Project>{

    /**
     * 对于绝大部分的使用场景来说,这个Object就是Project
     * */
    @Override
    void apply(Project project) {
        10.times{i ->
            //这里的task是project的方法
            project.task("supermanman_"+i){
                dependsOn('fc')
                def temp = i
                doLast{
                    println("Executing task ${temp}")
                }
            }
        }
    }
}

apply plugin:MyPlugin
本质调用的是这个方法void apply(Map<String, ?> var1); 参数是个map
去语法糖就是 apply([plugin:MyPlugin])
apply中map的key还可以是其他的,比如:

apply from: 'maven_pub.gradle'

我们看下apply方法定义在哪里

public interface PluginAware {
    PluginContainer getPlugins();

    void apply(Closure var1);

    void apply(Action<? super ObjectConfigurationAction> var1);

    void apply(Map<String, ?> var1);

    PluginManager getPluginManager();
}


public interface Project extends Comparable<Project>, ExtensionAware, PluginAware {...

可以看到Project类中继承了这个接口,所以在gradle文件中可以调用到这个方法

下面我们梳理下上方插件的调用逻辑:

  1. apply plugin:MyPlugin 当我们这么写的时候,就相当于调用了apply这个方法,传进去了一个map参数。
  2. 当我们执行到这个apply时,就会查找这个plugin的java类,然后调用它的apply方法执行一遍

这个plugin可以放到公共的服务器,不放在这里,这样使用的时候就可以

apply plugin:"http://myserver.com/my-script"

gradle会去下载这个脚本。

如何引用类

image.png

image.png

如果想在构建中使用某个类,而不是在编译中使用应该怎么办:
第二个阶段:configuiation阶段会把build.gradle从上到下跑一边,相当于把build.gradle放在JVM中执行一遍,
第三个阶段:execution阶段,才会去执行任务---compileJava,这个阶段才会去使用src/main/java/.....java中的类。

所以classpath是分两种的,一部分是build.gradle / build script中需要的classpath,一部分是compile java所需要的classpath(上面讲述的在dependences中声明的依赖就是解决compile java这个问题的)。

如果相为构建脚本声明classpath ,需要在buildscript中去声明仓库和依赖

image.png

image.png

如果我们想要在脚本中引入插件,apply plugin:xxxx ,如果插件在远端,则需要在buildscript中的dependencies中进行引用,后面才可以使用apply plugin:xxxx

buildscript {
    repositories {
        jcenter() {
        }
        maven {
        }
        jcenter()
        maven { url "https://xxxxx" }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:4.0.0'
        classpath 'com.xxxx.xxx:xxx-aar:1.0.1'
        classpath "com.xxx.xxx.gradle:gradle-xxxx-plugin:0.0.1"
    }
}

先在repositories中声明插件所在的仓库地址

再在dependencies中声明依赖

如何搜索依赖库

mvnrepository.com/artifact/co…

image.png

apply plugin: 'com.android.application'

是如何被引入的呢

这样我们就可以去使用这个classpath下载下来的所有插件了。
我们可以去上方的仓库中把它下载下来

image.png 如何通过apply plugin: 'com.android.application'找到对应的插件呢 当我们调用apply plugin: 'com.android.application'时,我们会通知as我们要使用这个插件,as会去dependencise下的buildscirpt中去寻找里面的classpath里是否包含这个插件,

image.png

image.png

image.png

implementation-class=com.android.build.gradle.AppPlugin

表明要使用这个插件,请你去找AppPlugin这个类,所以gradle的jvm就成功的加载到了AppPlugin这个类并实例化,并调用这个类的实例方法的apply()方法 可以去google搜索这个类AppPlugin

相关网站:
【搜索依赖库的网站】
下载gradle
gradle官方文档
官方文档--Gradle生命周期
【github】gradle源代码
【必看】groovy语言学习(groovy dsl)
plugin官方文档

构建逻辑的复用

简单插件

script插件

buildSrc插件

发布的插件

实际插件分析