阅读 2007

Gradle插件从入门到进阶

Gradle插件从入门到进阶

首发于我的blog xsfelvis.github.io/2019/05/02/…

Transform相关请参考 xsfelvis.github.io/2019/05/02/…

1、简介

Gradle本身的领域对象主要有Project和Task。Project为Task提供了执行上下文,所有的Plugin要么向Project中添加用于配置的Property,要么向Project中添加不同的Task。一个Task表示一个逻辑上较为独立的执行过程,比如编译Java源代码,拷贝文件,打包Jar文件,甚至可以是执行一个系统命令或者调用Ant。另外,一个Task可以读取和设置Project的Property以完成特定的操作。

Groovy 基础

Android DSL 基础

相关代码在 github.com/xsfelvis/Gr…

2、核心概念

Project对象

自定义插件类是通过实现Plugin 接口,并将 org.gradle.api.Project作为模板参数,其中org.gradle.api.Project的实例对象将作为参数传给void apply(Project project)函数,根据官网,可以看出Project是与Gradle交互的主接口,通过Project可以使用gradle的所有特性,并且 Project与build.grale是一对一的关系。简而言之,就是通过代码使用Gradle,通过Project这个入口即可

我们对project的理解更多来源于项目目录中的build.gradle文件(因为它其实就是project对象的委托,在脚本中的配置方法都对应着Project中的API,当构建进程启动后Gradle基于build.gradle中的配置实例化org.gradle.api.Project类,本质上可以认为是包含多个Task的容器,所有的Task都存放在TaskContainer中,Project对象的类图如下所示:

image.png

项目配置

在build.gradle脚本文件中,我们不仅可以对单独project进行配置,也可以定义project块的共有逻辑等,参考下面的定义。

image.png

常见的例子

// 为所有项目添加仓库源配置
allprojects {
    repositories {
        jcenter()
        google()
    }
}
// 为所有子项目添加mavenPublish的配置块
subprojects {
    mavenPublish {
        groupId = maven.config.groupId
        releaseRepo = maven.config.releaseRepo
        snapshotRepo = maven.config.snapshotRepo
    }
}
复制代码

Task

Gradle Task API

Gradle构建脚本默认的名字是build.gradle,当在shell中执行gradle命令时,Gradle会去当前目录下寻找名字是build.gradle的文件。在Gradle中一个原子性的操作叫做task,简单理解为task是Gradle脚本中的最小可执行单元。

下面是task的类图。

image.png

Task的Actions

一个Task是由一序列Action组成的,当运行一个Task的时候,这个Task里的Action序列会按照顺序执行

Task的几种常见写法

task myTask1 {
    doLast {
        println "doLast in task1"
    }
}

task myTask2 << {
    println "doLast in task2"
}

//采用 Project.task(String name) 方法来创建
project.task("myTask3").doLast {
    println "doLast in task3"
}

//采用 TaskContainer.create(String name) 方法来创建
project.tasks.create("myTask4").doLast {
    println "doLast in task4"
}

project.tasks.create("myTask5") << {
    println "doLast in task5"
}




复制代码

目前task的动作(action)声明主要包含两个方法:

  • doFirst  等价操作 缩写 leftShift <<(5.0会废弃)
  • doLast

在 Gradle 中定义 Task 的时候,可以指定更多的参数,如下所示:

参数名 含义 默认值
name task的名字 必须指定,不能为空
type task的父类 默认值为org.gradle.api.DefaultTask
overwrite 是否替换已经存在的同名task false
group task所属的分组名 null
description task的描述 null
dependsOn task依赖的task集合
constructorArgs 构造函数参数

Task的依赖

gradle中任务的执行顺序是不确定的。通过task之间的依赖关系,gradle能够确保所依赖的task会被当前的task先执行。使用task的dependsOn()方法,允许我们为task声明一个或者多个task依赖。

task first{
    doLast{
        println("first")
    }
}

task second{
    doLast{
        println("second")
    }
}

task third{
    doLast{
        println("third")
    }
}

task test(dependsOn:[second,first]){
    doLast{
        println("first")
    }
}

third.dependsOn(test)
复制代码

执行顺序

> Task :app:first 


> Task :app:second 


> Task :app:test1 


> Task :app:third 
复制代码

Task的类型

有copy、jar、Delete 等等 可以参考Doc文档

task copyFile(type: Copy) {
   from 'xml'
   into 'destination'
}

自定义Task

Gradle 中通过 task 关键字创建的 task,默认的父类都是 org.gradle.api.DefaultTask,这里定义了一些 task 的默认行为。看看下面这个例子:

//自定义Task类,必须继承自DefaultTask
class SayHelloTask extends DefaultTask {
    
    String msg = "default name"
    int age = 18        

    //构造函数必须用@javax.inject.Inject注解标识
    @javax.inject.Inject
    SayHelloTask(int age) {
        this.age = age
    }

    //通过@TaskAction注解来标识该Task要执行的动作
    @TaskAction
    void sayHello() {
        println "Hello $msg ! age is ${age}"
    }

}

//通过constructorArgs参数来指定构造函数的参数值
task hello1(type: SayHelloTask, constructorArgs: [30])

//通过type参数指定task的父类,可以在配置代码里修改父类的属性
task hello2(type: SayHelloTask, constructorArgs: [18]) {
        //配置代码里修改 SayHelloTask 里的字段 msg 的值
    msg = "hjy"
}
复制代码


执行这两个task

> Task :hello1
Hello default name ! age is 30

> Task :hello2
Hello hjy ! age is 18
复制代码

### Task的类图 Gradle所说的Task是org.gradle.api.Task接口,默认实现是org.gradle.api.DefaultTask类,其类图如下
![image.png](https://cdn.nlark.com/yuque/0/2019/png/215777/1556193021427-9d3cda16-11cd-4908-8f9c-aac56947ad6e.png#align=left&display=inline&height=806&name=image.png&originHeight=1081&originWidth=1000&size=214151&status=done&width=746)

TaskContainer接口解析

TaskContianer 是用来管理所有的 Task 实例集合的,可以通过 Project.getTasks() 来获取 TaskContainer 实例。

org.gradle.api.tasks.TaskContainer接口:
//查找task
findByPath(path: String): Task
getByPath(path: String): Task
getByName(name: String): Task
withType(type: Class): TaskCollection
matching(condition: Closure): TaskCollection

//创建task
create(name: String): Task
create(name: String, configure: Closure): Task 
create(name: String, type: Class): Task
create(options: Map<String, ?>): Task
create(options: Map<String, ?>, configure: Closure): Task

//当task被加入到TaskContainer时的监听
whenTaskAdded(action: Closure)


//当有task创建时
getTasks().whenTaskAdded { Task task ->
    println "The task ${task.getName()} is added to the TaskContainer"
}

//采用create(name: String)创建
getTasks().create("task1")

//采用create(options: Map<String, ?>)创建
getTasks().create([name: "task2", group: "MyGroup", description: "这是task2描述", dependsOn: ["task1"]])

//采用create(options: Map<String, ?>, configure: Closure)创建
getTasks().create("task3", {
    group "MyGroup"
    setDependsOn(["task1", "task2"])
    setDescription "这是task3描述"
})

复制代码

默认情况下,我们常见的task都是org.gradle.api.DefaultTask类型。但是在gradle当中有相当丰富的task类型我们可以直接使用。要更改task的类型,我们可以参考下面的示例

task createDistribution(type:Zip){
    
}
复制代码

更多关于task的类型,可以参考gradle的官方文档

www.jianshu.com/p/cd1a78dc8…
www.ezlippi.com/blog/2015/0…
blog.csdn.net/lzyzsd/arti…

Task的增量构建

Gradle 支持一种叫做 up-to-date 检查的功能,也就是常说的增量构建。Gradle 的 Task 会把每次运行的结果缓存下来,当下次运行时,会检查输出结果有没有变更,如果没有变更则跳过运行,这样可以提高 Gradle 的构建速度。
通常,一个 task 会有一些输入(inputs)和一些输出(outputs),task 的输入会影响其输出结果,以官网中的一张图为例:

image.png

图中表示一个java编译的task,它的输入有2种,一是JDK版本号,一是源文件,它的输出结果为class文件,只要JSK版本号与源文件有任何变动,最终编译出的class文件肯定不同的。当我们执行过一次·编译任务之后,再次运行该task,如果发现他的输入没有任何改动,那么它编译后的结果肯定也是不变的,可以直接从缓存里获取输出,这样Gradle会标识该task为UP-TO-DATE,从而跳过该task的执行

TaskInputs、TaskOutputs介绍

如何实现一个增量构建呢,至少指定一个输入,一个输出,Task.getInputs() 对象类型为 TaskInputs,Task.getOutputs() 对象类型为 TaskOuputs,从中也可以看到inputs、outputs都支持哪些数据类型

task test1 {
    //设置inputs
    inputs.property("name", "hjy")
    inputs.property("age", 20)
    //设置outputs
    outputs.file("$buildDir/test.txt")

    doLast {
        println "exec task task1"
    }
}

task test2 {
    doLast {
        println "exec task task2"
    }
}

//第一次的运行结果
> Task :test1
exec task task1

> Task :test2
exec task task2

BUILD SUCCESSFUL in 0s
2 actionable tasks: 2 executed

//第二次的运行结果
> Task :test2
exec task task2

BUILD SUCCESSFUL in 0s
2 actionable tasks: 1 executed, 1 up-to-date
复制代码

从结果中可以看到,第2次运行时,test1 task 并没有运行,而是被标记为 up-to-date,而 test2 task 则每次都会运行,这就是典型的增量构建。

taskInputs、taskOutputs注解

可以通过task注解来实现增量构建,这是一种更加灵活方便的方式

注解名 属性类型 描述
@Input 任意Serializable类型 一个简单的输入值
@InputFile File 一个输入文件,不是目录
@InputDirectory File 一个输入目录,不是文件
@InputFiles Iterable File列表,包含文件和目录
@OutputFile File 一个输出文件,不是目录
@OutputDirectory File 一个输出目录,不是文件
@OutputFiles Map<String, File>或Iterable 输出文件列表
@OutputDirectories Map<String, File>或Iterable 输出目录列表
class SayHelloTask extends DefaultTask {
    
    //定义输入
    @Input
    String username;
    @Input
    int age

    //定义输出
    @OutputDirectory
    File destDir;

    @TaskAction
    void sayHello() {
        println "Hello $username ! age is $age"
    }

}

task test(type: SayHelloTask) {
    age = 18
    username = "hjy"
    destDir = file("$buildDir/test")
}
复制代码

Property

ext命名空间

Gradle中很多模型类都提供了特别的属性支持,比如Project.在gradle内部,这些属性会以键值对的形式存储。使用ext命名空间,我们可以方便的添加属性。下面的方式都是支持的:

//在project中添加一个名为groupId的属性
project.ext.groupId="tech.easily"
// 使用ext块添加属性
ext{
    artifactId='EasyDependency'
    config=[
            key:'value'
    ]
}
复制代码

值得注意的是,只有在声明属性的时候我们需要使用ext命名空间,在使用属性的时候,ext命名空间是可以省略的。

属性文件

正如我们经常在Android项目中看到的,我们可以在项目的根目录下新建一个gradle.properties文件,并在文件中定义简单的键值对形式的属性。这些属性能够被项目中的gradle脚本所访问。如下所示:

# gradle.properties
# 注意文件的注释是以#开头的
groupId=tech.easily
artifactId=EasyDependency
复制代码
复制代码

有的时候,我们可能需要在代码中动态的创建属性文件并读取文件中的属性(比如自定义插件的时候),我们可以使用java.util.Properties类。比如:

void createPropertyFile() {
    def localPropFile = new File(it.projectDir.absolutePath + "/local.properties")
    def defaultProps = new Properties()
    if (!localPropFile.exists()) {
        localPropFile.createNewFile()
        defaultProps.setProperty("debuggable", 'true')
        defaultProps.setProperty("groupId", GROUP)
        defaultProps.setProperty("artifactId", project.name)
        defaultProps.setProperty("versionName", VERSION_NAME)
        defaultProps.store(new FileWriter(localPropFile), "properties auto generated for resolve dependencies")
    } else {
        localPropFile.withInputStream { stream ->
            defaultProps.load(stream)
        }
    }
}
复制代码

关于属性很重要的一点是属性是可以继承的。在一个项目中定义的属性会自动的被其子项目继承,不管我们是用以上哪种方式添加属性都是适用的。

ExtensionContainer

Extension简介

就是 Gradle 的 Extension,翻译成中文意思就叫扩展。它的作用就是通过实现自定义的 Extension,可以在 Gradle 脚本中增加类似 android 这样命名空间的配置,Gradle 可以识别这种配置,并读取里面的配置内容。

一般我们通过ExtensionContainer来创建Extension,这个类跟TaskContainer命名有点类似。TaskContainer是用来创建并管理Task的,而ExtensionContainer则是用来创建并管理Extension的,通过Project的以下API可以获取到ExtensionContainer对象

ExtensionContainer getExtensions()
复制代码

简单的Extension

/先定义一个普通的java类,包含2个属性
class Foo {
    int age
    String username
    String toString() {
        return "name = ${username}, age = ${age}"
    }
}
//创建一个名为 foo 的Extension
getExtensions().create("foo", Foo)
//配置Extension
foo {
    age = 30
    username = "hjy"
}
task testExt.doLast {
    //能直接通过 project 获取到自定义的 Extension
    println project.foo
}
复制代码

foo 就是我们自定义的 Extension 了,它里面能配置的属性与类 Foo 中的字段是一致的,在 build.gradle 中可以直接通过 project.foo 来访问。每个 Extension 实际上与某个类是相关联的,在 build.gradle 中通过 DSL 来定义,Gradle 会识别解析并生成一个对象实例,通过该类可以获取我们所配置的信息。
 Project 有个扩展属性是通过 ext 命名空间配置的,可以看到 ext 与这里是类似的,不同的是 ext 可以配置任何键值对的属性值,而这里只能识别我们定义的 Java 类里的属性值。

ExtensionContainer主要api及用法

<T> T create(String name, Class<T> type, Object... constructionArguments)
<T> T create(Class<T> publicType, String name, Class<? extends T> instanceType, Object... constructionArguments)
复制代码

先来看看后面这个 API 所有参数的含义。

  • publicType:创建的 Extension 实例暴露出来的类类型;
  • name:要创建的Extension的名字,可以是任意符合命名规则的字符串,不能与已有的重复,否则会抛异常;
  • instanceType:该Extension的类类型;
  • constructionArguments:类的构造函数参数值

官方文档里还说明了一个特性,创建的 Extension 对象都默认实现了 ExtensionAware 接口,并注明出处。

示例

//父类
class Animal {
    
    String username
    int legs

    Animal(String name) {
        username = name
    }
    
    void setLegs(int c) {
        legs = c
    }

    String toString() {
        return "This animal is $username, it has ${legs} legs."
    }
}

//子类
class Pig extends Animal {
    
    int age
    String owner

    Pig(int age, String owner) {
        super("Pig")
        this.age = age
        this.owner = owner
    }

    String toString() {
        return super.toString() + " Its age is $age, its owner is $owner."
    }

}

//创建的Extension是 暴露出来Animal 类型,创建extension名称是name,该extension的类型是Pig,后面2个是参数
Animal aAnimal = getExtensions().create(Animal, "animal", Pig, 3, "hjy")
//创建的Extension是 Pig 类型
Pig aPig = getExtensions().create("pig", Pig, 5, "kobe")

animal {
    legs = 4    //配置属性
}

pig {
    setLegs 2   //这个是方法调用,也就是 setLegs(2)
}

task testExt << {
    println aAnimal
    println aPig
    //验证 aPig 对象是 ExtensionAware 类型的
    println "aPig is a instance of ExtensionAware : ${aPig instanceof ExtensionAware}"
}
复制代码

增加Extension

  • create() 方法会创建并返回一个 Extension 对象,
  • add() 方法,唯一的差别是它并不会返回一个 Extension 对象

基于前面的这个实例,我们可以换一种写法如下:

getExtensions().add(Pig, "mypig", new Pig(5, "kobe"))
mypig {
    username = "MyPig"
    legs = 4
    age = 1
}
task testExt << {
    def aPig = project.getExtensions().getByName("mypig")
    println aPig
}
复制代码

查找Extension

Object findByName(String name)
<T> T findByType(Class<T> type)
Object getByName(String name)       //找不到会抛异常
<T> T getByType(Class<T> type)  //找不到会抛异常
复制代码

嵌套Extension 方式一

类似下面这样的配置应该随处可见:

outer {
    
    outerName "outer"
    msg "this is a outer message."
    inner {
        innerName "inner"
        msg "This is a inner message."
    }
    
}
复制代码

可以通过下面的方式来创建

class OuterExt {
    
    String outerName
    String msg
    InnerExt innerExt = new InnerExt()

    void outerName(String name) {
        outerName = name
    }

    void msg(String msg) {
        this.msg = msg
    }
    
    //创建内部Extension,名称为方法名 inner
    void inner(Action<InnerExt> action) {
        action.execute(inner)
    }

    //创建内部Extension,名称为方法名 inner
    void inner(Closure c) {
        org.gradle.util.ConfigureUtil.configure(c, innerExt) 
    }

    String toString() {
        return "OuterExt[ name = ${outerName}, msg = ${msg}] " + innerExt
    }

}


class InnerExt {
    
    String innerName
    String msg

    void innerName(String name) {
        innerName = name
    }

    void msg(String msg) {
        this.msg = msg
    }

    String toString() {
        return "InnerExt[ name = ${innerName}, msg = ${msg}]"
    }

}

def outExt = getExtensions().create("outer", OuterExt)

outer {
    
    outerName "outer"
    msg "this is a outer message."

    inner {
        innerName "inner"
        msg "This is a inner message."
    }

}

task testExt doLast {
    println outExt
}
复制代码

关键在以下下面的方法

void inner(Action<InnerExt> action)
void inner(Closure c)
复制代码

定义在outer内部的inner,Gradle 解析时本质上会调用 outer.inner(……)方法,该方法的参数是一个闭包(Script Block) 所以在类OuterExt中必须定义inner方法

嵌套Extension方式二(NamedDomainObjectContainer)

使用场景

Gradle Extension 的时候,说到名为 android 的 Extension 是由 BaseExtension 这个类来实现的,里面对 buildTypes 是这样定义的:

private final NamedDomainObjectContainer<BuildType> buildTypes;
复制代码

buildTypes 就是 NamedDomainObjectContainer 类型的,先来看看 buildTypes 在 Android 中是怎么使用的,下面这段代码应该都很熟悉了,它定义了 debug、relase 两种打包模式:

android {
    buildTypes {
        release {
            // 是否开启混淆
            minifyEnabled true
            // 开启ZipAlign优化
            zipAlignEnabled true
            //去掉不用资源
            shrinkResources true
            // 混淆文件位置
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            // 使用release签名
            signingConfig signingConfigs.hmiou
        }
        debug {
            signingConfig signingConfigs.hmiou
        }
    }
}
复制代码

当我们新建一个项目时候,默认会有debug和release这2个配置,那么debug、release可以修改其他名字吗,能增加其他名字来配置吗,比如想增加一个测试包配置test,还有就是release里面都能配置哪些属性呢
我来说下结果,如果不确定的,可以实际验证一下:

  • debug、release 是可以修改成其他名字的,你可以替换成你喜欢的名字;
  • 你可以增加任意不同名字的配置,比如增加一个开发版本的打包配置 dev ;
  • 可配置的属性可参考接口:com.android.builder.model.BuildType ;

可以看到它是非常灵活的,可以根据不同的场景定义不同的配置,每个不同的命名空间都会生成一个 BuildType 配置。要实现这样的功能,必须使用 NamedDomainObjectContainer 类型。

什么是NamedDomainObjectContainer

顾名思义就是命名领域对象容器,它的主要功能有:

  • 通过DSL创建指定type的对象实例
  • 指定的type必须有一个public构造函数,且必须带有一个String name的参数
  • 它是一个实现了SortedSet接口的容器,所以所有领域对象的name属性都必须是唯一的,在容器内部会用name属性来排序

named domain object container is a specialisation of NamedDomainObjectSet that adds the ability to create instances of the element type.  Note that a container is an implementation of SortedSet, which means that the container is guaranteed to only contain elements with unique names within this container. Furthermore, items are ordered by their name.

创建NamedDomainObjectContainer

NamedDomainObjectContainer 需要通过 Project.container(...) API 来创建,其定义为:

<T> NamedDomainObjectContainer<T> container(Class<T> type)
<T> NamedDomainObjectContainer<T> container(Class<T> type, NamedDomainObjectFactory<T> factory)
<T> NamedDomainObjectContainer<T> container(java.lang.Class<T> type, Closure factoryClosure
复制代码

来看个具体的实例:

//这是领域对象类型定义
class TestDomainObj {
    
    //必须定义一个 name 属性,并且这个属性值初始化以后不要修改
    String name

    String msg

    //构造函数必须有一个 name 参数
    public TestDomainObj(String name) {
        this.name = name
    }

    void msg(String msg) {
        this.msg = msg
    }

    String toString() {
        return "name = ${name}, msg = ${msg}"
    }
}

//创建一个扩展
class TestExtension {

    //定义一个 NamedDomainObjectContainer 属性
    NamedDomainObjectContainer<TestDomainObj> testDomains

    public TestExtension(Project project) {
        //通过 project.container(...) 方法创建 NamedDomainObjectContainer 
        NamedDomainObjectContainer<TestDomainObj> domainObjs = project.container(TestDomainObj)
        testDomains = domainObjs
    }

    //让其支持 Gradle DSL 语法
    void testDomain(Action<NamedDomainObjectContainer<TestDomainObj>> action) {
        action.execute(testDomains)
    }

    void test() {
        //遍历命名领域对象容器,打印出所有的领域对象值
        testDomains.all { data ->
            println data        
        }
    }
}

//创建一个名为 test 的 Extension
def testExt = getExtensions().create("test", TestExtension, project)

test {
    testDomain {
        domain2 {
            msg "This is domain2"
        }
        domain1 {
            msg "This is domain1"
        }
        domain3 {
            msg "This is domain3"
        }
    }   
}

task myTask doLast {
    testExt.test()
}
复制代码

运行结果如下:

name = domain1, msg = This is domain1
name = domain2, msg = This is domain2
name = domain3, msg = This is domain3
复制代码

查找和遍历

NamedDomainObjectContainer 既然是一个容器类,与之相应的必然会有查找容器里的元素和遍历容器的方法:

//遍历
void all(Closure action)
//查找
<T> T getByName(String name)
//查找
<T> T findByName(String name)
复制代码

还是接着前面的例子:

//通过名字查找
TestDomainObj testData = testDomains.getByName("domain2")
println "getByName: ${testData}"
//遍历命名领域对象容器,打印出所有的领域对象值
testDomains.all { data ->
    println data        
}
复制代码

需要注意的是,Gradle 中有很多容器类的迭代遍历方法有 each(Closure action)、all(Closure action),但是一般我们都会用 all(...) 来进行容器的迭代。all(...) 迭代方法的特别之处是,不管是容器内已存在的元素,还是后续任何时刻加进去的元素,都会进行遍历。

Android的Extension

我们在gradle中会看到 android{}

image.png

defaultConfig、productFlavors、signingConfigs、buildTypes 这4个内部 Extension对象是怎么定义的,通过查看源码可以找到一个叫 BaseExtension 的类,里面的相关代码如下:

private final DefaultConfig defaultConfig;
    private final NamedDomainObjectContainer<ProductFlavor> productFlavors;
    private final NamedDomainObjectContainer<BuildType> buildTypes;
    private final NamedDomainObjectContainer<SigningConfig> signingConfigs;
    public void defaultConfig(Action<DefaultConfig> action) {
        this.checkWritability();
        action.execute(this.defaultConfig);
    }
    
     public void buildTypes(Action<? super NamedDomainObjectContainer<BuildType>> action) {
        this.checkWritability();
        action.execute(this.buildTypes);
    }
    public void productFlavors(Action<? super NamedDomainObjectContainer<ProductFlavor>> action) {
        this.checkWritability();
        action.execute(this.productFlavors);
    }
    public void signingConfigs(Action<? super NamedDomainObjectContainer<SigningConfig>> action) {
        this.checkWritability();
        action.execute(this.signingConfigs);
    }
复制代码

在 app 的 build.gradle 里我们通常会采用插件 apply plugin: 'com.android.application' ,而在 library module 中则采用插件 apply plugin: 'com.android.library',AppPlugin 就是插件 com.android.application 的实现类,LibraryPlugin 则是插件 com.android.library 的实现类,接着再看看 AppPlugin 里是怎样创建 Extension 的:

public class AppPlugin extends BasePlugin implements Plugin<Project> {
    @Inject
    public AppPlugin(Instantiator instantiator, ToolingModelBuilderRegistry registry) {
        super(instantiator, registry);
    }
    protected BaseExtension createExtension(Project project, ProjectOptions projectOptions, Instantiator instantiator, AndroidBuilder androidBuilder, SdkHandler sdkHandler, NamedDomainObjectContainer<BuildType> buildTypeContainer, NamedDomainObjectContainer<ProductFlavor> productFlavorContainer, NamedDomainObjectContainer<SigningConfig> signingConfigContainer, NamedDomainObjectContainer<BaseVariantOutput> buildOutputs, ExtraModelInfo extraModelInfo) {
        return (BaseExtension)project.getExtensions().create("android", AppExtension.class, new Object[]{project, projectOptions, instantiator, androidBuilder, sdkHandler, buildTypeContainer, productFlavorContainer, signingConfigContainer, buildOutputs, extraModelInfo});
    }
    public void apply(Project project) {
        super.apply(project);
    }
    //省略...
}
复制代码

在 createExtension() 方法中,可以看到创建了一个名为 android 的 Extension,该 Extension 的类型为 AppExtension,而 AppExtension 的继承结构为 AppExtension -> TestedExtension -> BaseExtension,所以它的实现逻辑大部分都是在 BaseExtension 里实现的。

在Android 工程中的build.gradle 文件中,我们配置相关信息使用 android{} 节点,从 AppPlugin 也能看出其 Extension的名称为 android ,所以获取方法如下:

  • project.extensions.getByName
  • project.extensions.getByType

def getInfo() {
		//或者 直接 project.android
    BaseExtension extension = project.extensions.getByName("android")
    def android = project.extensions.getByType(AppExtension)
    project.android
    
    println "buildToolsVersion:${extension.buildToolsVersion}"
    println "compileSdkVersion:${extension.getCompileSdkVersion()}"
    println "applicationId:${extension.defaultConfig.applicationId}"
    println "minSdkVersion:${extension.defaultConfig.minSdkVersion}"
    println "targetSdkVersion:${extension.defaultConfig.targetSdkVersion}"
    println "versionCode:${extension.defaultConfig.versionCode}"
    println "versionName:${extension.defaultConfig.versionName}"
}
复制代码

更详细的请参考

3、构建生命周期

三个阶段

每次构建的本质其实就是执行一系列的Task,某些Task可能依赖其他Task,那些没有依赖的Task总会被最先执行,而且每个Task只会被执行一遍,每次构建的依赖关系是在构建的配置阶段确定的,在gradle构建中,构建的生命周期主要包括以下三个阶段:

初始化(Initialization)

构建工具会根据每个build.gradle文件创建出一个Project实例,初始化阶段会执行项目根目录下的Settings.gradle文件,来分析哪些项目参与构建

include ':app'

include ':libraries:someProject'

配置(Configuration)

这个阶段通过执行构建脚本来为每个project创建并分配Task。配置阶段会去加载所有参与构建的项目的build.gradle文件,会将build.gradle文件实例化为一个Gradle的project对象,然后分析project之间的依赖关系,下载依赖文件,分析project下的task之间的依赖关系

执行(Execution)

这是Task真正被执行的阶段,Gradle会根据依赖关系决定哪些Task需要被执行,以及执行的先后顺序。
task是Gradle中的最小执行单元,我们所有的构建,编译,打包,debug,test等都是执行了某一个task,一个project可以有多个task,task之间可以互相依赖。例如我有两个task,taskA和taskB,指定taskA依赖taskB,然后执行taskA,这时会先去执行taskB,taskB执行完毕后在执行taskA。

image.png

在根目录和app目录下的build.gradle中会引用下面的插件

dependencies { classpath 'com.android.tools.build:gradle:2.2.2' }

apply plugin: 'com.android.application'

image.png

Android 三个文件重要的gradle

Gradle项目有3个重要的文件需要深入理解:

  • settings.gradle

settings.gradle 文件会在构建的 initialization 阶段被执行,它用于告诉构建系统哪些模块需要包含到构建过程中。对于单模块项目, settings.gradle 文件不是必需的。对于多模块项目,如果没有该文件,构建系统就不能知道该用到哪些模块。

  • 项目根目录的 build.gradle

项目根目录的 build.gradle 文件用来配置针对所有模块的一些属性。它默认包含2个代码块:buildscript{…}和allprojects{…}。前者用于配置构建脚本所用到的代码库和依赖关系,后者用于定义所有模块需要用到的一些公共属性。

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

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

复制代码

buildscript:定义了 Android 编译工具的类路径。repositories中, jCenter是一个著名的 Maven 仓库。
allprojects:中定义的属性会被应用到所有 moudle 中,但是为了保证每个项目的独立性,我们一般不会在这里面操作太多共有的东西。

  • 模块目录的 build.gradle

模块级配置文件 build.gradle 针对每个moudle 的配置,如果这里的定义的选项和顶层 build.gradle定义的相同。它有3个重要的代码块:plugin,android 和 dependencies。

常用gradle命令

//构建
gradlew app:clean    //移除所有的编译输出文件,比如apk

gradlew app:build   //构建 app module ,构建任务,相当于同时执行了check任务和assemble任务

//检测
gradlew app:check   //执行lint检测编译。

//打包
gradlew app:assemble //可以编译出release包和debug包,可以使用gradlew assembleRelease或者gradlew assembleDebug来单独编译一种包

gradlew app:assembleRelease  //app module 打 release 包

gradlew app:assembleDebug  //app module 打 debug 包

//安装,卸载

gradlew app:installDebug  //安装 app 的 debug 包到手机上

gradlew app:uninstallDebug  //卸载手机上 app 的 debug 包

gradlew app:uninstallRelease  //卸载手机上 app 的 release 包

gradlew app:uninstallAll  //卸载手机上所有 app 的包

复制代码

监听生命周期

在gradle的构建过程中,gradle为我们提供了非常丰富的钩子,帮助我们针对项目的需求定制构建的逻辑,如下图所示:

image.png

要监听这些生命周期,主要有两种方式:

  • 添加监听器
  • 使用钩子的配置块

关于可用的钩子可以参考GradleProject中的定义,常用的钩子包括:

Project

Project提供的生命周期回调方法有

//在 Project 进行配置前调用
void beforeEvaluate(Closure closure)
//在 Project 配置结束后调用
void afterEvaluate(Closure closure)
复制代码

beforeEvaluate 必须在父模块的 build.gradle 对子模块进行配置才能生效,因为在当前模块的 build.gradle 中配置,它自己本身都没配置好,所以不会监听到。

settings.gradle 代码:

include ":app"
复制代码

build.gradle 代码:

//对子模块进行配置
subprojects { sub ->
    sub.beforeEvaluate { proj ->
        println "子项目beforeEvaluate回调..."
    }
}
println "根项目配置开始---"
task rootTest {
    println "根项目里任务配置---"
    doLast {
        println "执行根项目任务..."
    }
}
println "根项目配置结束---"
复制代码

app/build.gradle 代码:

println "APP子项目配置开始---"
afterEvaluate {
    println "APP子项目afterEvaluate回调..."
}
task appTest {
    println "APP子项目里任务配置---"
    doLast {
        println "执行子项目任务..."
    }
}
println "APP子项目配置结束---"
复制代码

在根目录执行:gradle -q,结果如下:

根项目配置开始---
根项目里任务配置---
根项目配置结束---
子项目beforeEvaluate回调...
APP子项目配置开始---
APP子项目里任务配置---
APP子项目配置结束---
APP子项目afterEvaluate回调...
复制代码

project.android 获取到AppExtension:

Gradle

Gradle 提供的生命周期回调方法很多,部分与 Project 里的功能雷同:

//在project进行配置前调用,child project必须在root project中设置才会生效,root project必须在settings.gradle中设置才会生效
void beforeProject(Closure closure)
//在project配置后调用
afterProject(Closure closure)
//构建开始前调用
void buildStarted(Closure closure)
//构建结束后调用
void buildFinished(Closure closure)
//所有project配置完成后调用
void projectsEvaluated(Closure closure)
//当settings.gradle中引入的所有project都被创建好后调用,只在该文件设置才会生效
void projectsLoaded(Closure closure)
//settings.gradle配置完后调用,只对settings.gradle设置生效
void settingsEvaluated(Closure closure)
复制代码
  • beforeProject()/afterProject()
    等同于Project中的beforeEvaluateafterEvaluate
  • settingsEvaluated()
    settings脚本被执行完毕,Settings对象配置完毕
  • projectsLoaded()
    所有参与构建的项目都从settings中创建完毕
  • projectsEvaluated()
    所有参与构建的项目都已经被评估完

我们修改 setting.gradle 的代码如下:

gradle.settingsEvaluated {
    println "settings:执行settingsEvaluated..."
}
gradle.projectsLoaded {
    println "settings:执行projectsLoaded..."
}
gradle.projectsEvaluated {
    println "settings: 执行projectsEvaluated..."
}
gradle.beforeProject { proj ->
    println "settings:执行${proj.name} beforeProject"
}
gradle.afterProject { proj ->
    println "settings:执行${proj.name} afterProject"
}
gradle.buildStarted {
    println "构建开始..."
}
gradle.buildFinished {
    println "构建结束..."
}
include ":app"
复制代码

这个时候的执行结果如下:

settings:执行settingsEvaluated...
settings:执行projectsLoaded...
settings:执行test beforeProject
根项目配置开始---
根项目里任务配置---
根项目配置结束---
settings:执行test afterProject
settings:执行app beforeProject
子项目beforeEvaluate回调...
APP子项目配置开始---
APP子项目里任务配置---
APP子项目配置结束---
settings:执行app afterProject
APP子项目afterEvaluate回调...
settings: 执行projectsEvaluated...
构建结束...
复制代码

可以看到 gradle.beforeProject 与 project.beforeEvaluate 是类似的,同样 afterProject 与 afterEvaluate 也是类似的。

除此之外,Gradle 还有一个通用的设置生命周期监听器的方法:addListener

image.png

上面的 BuildListener、ProjectEvaluationListener 等与前面的部分 API 功能是一致的,这里不再赘述了。

TaskExecutionGraph(Task执行图)

Gradle 在配置完成后,会对所有的 task 生成一个有向无环图,这里叫做 task 执行图,他们决定了 task 的执行顺序等。同样,Gradle 可以对 task 的执行生命周期进行监听。

//任务执行前掉用
void afterTask(Closure closure)
//任务执行后调用
void beforeTask(Closure closure)
//所有需要被执行的task已经task之间的依赖关系都已经确立
void whenReady(Closure closure)
复制代码

通过 gradle.getTaskGraph() 方法来获取 task 执行图:

TaskExecutionGraph taskGraph = gradle.getTaskGraph()
taskGraph.whenReady {
    println "task whenReady"
}
taskGraph.beforeTask { Task task ->
    println "任务名称:${task.name} beforeTask"
}
taskGraph.afterTask { Task task ->
    println "任务名称:${task.name} afterTask"
}
复制代码

生命周期回调的执行顺序:

gradle.settingsEvaluated->
gradle.projectsLoaded->
gradle.beforeProject->
project.beforeEvaluate->
gradle.afterProject->
project.afterEvaluate->
gradle.projectsEvaluated->
gradle.taskGraph.graphPopulated->
gradle.taskGraph.whenReady->
gradle.buildFinished
复制代码

4、自定义插件开发

三种方式

类型 说明
Build script 把插件写在 build.gradle 文件中,一般用于简单的逻辑,只在该 build.gradle 文件中可见
buildSrc 项目 将插件源代码放在 rootProjectDir/buildSrc/src/main/groovy 中,只对该项目中可见,适用于逻辑较为复杂
独立项目 一个独立的 Groovy 和 Java 项目,可以把这个项目打包成 Jar 文件包,一个 Jar 文件包还可以包含多个插件入口,将文件包发布到托管平台上,供其他人使用。本文将着重介绍此类。

具体从插件开发可以参考

需要注意的是 在main目录下创建
1、resources/META-INF/gradle-plugins文件夹,
2、在gradle-plugins文件夹下创建一个xxx.properties文件,(com.learntransform.testtransform.properties)
注意:这个xxx就是在app下的build.gradle中引入时的名字,例如:apply plugin: ‘xxx’(apply plugin:'com.learntransform.testtransform'
3、在文件书写引用到插件 implementation-class=me.xsfdev.learntransform.Hotfix

image.png

插件的本地化

  • 本地插件module

group = 'com.learntranform'
version = '1.0.1'
uploadArchives {
    repositories {
        flatDir {
            name "localRepository"
            dir "../app/localRepository/libs"
        }
    }
}
复制代码
  • 工程的gradle
buildscript {
    ext.kotlin_version = '1.2.41'
    repositories {
        flatDir {
            name 'localRepository'
            dir "app/localRepository/libs"
        }
        mavenLocal()
        jcenter()
        google()
    }
    dependencies {
        classpath(group: 'com.plugintest', name: 'hellodsl', version: '1.0.0') {
            changing = true
        }
        classpath(group: 'com.learntransform', name: 'learntransform', version: '1.0.1') {
            changing = true
        }
        classpath 'com.android.tools.build:gradle:3.1.2'
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        classpath 'com.novoda:bintray-release:0.8.0' //jcenter添加
        classpath 'com.xsfdev:complexcriptdsl:1.0.0'
    }
}
复制代码

完整代码可以参考 LearnGradle

5、常用方法

android.applicationVariants

更多参考需要看源码

android.applicationVariants 返回的是

  public DomainObjectSet<ApplicationVariant> getApplicationVariants() {
        return applicationVariantList;
   }
复制代码

一层层追 相关的有

image.png

在BaseVariant中
image.png

Task#upToDateWhen

每次都会编译

outputs.upToDateWhen { true } doesn't mean "the task is up-to-date." It just means that the outputs are up-to-date for that particular spec. Gradle will still do its own up-to-date checks.
The other thing that may be confusing is where the task's actions are defined. If the actions are defined in the build script, the build script itself is an input to the task. So changes to the build script will make the task out-of-date.
So if I had a task like:

task myTask {
    def outputFile = file("output.txt")
    outputs.file outputFile
    doLast {
        outputFile.text = "Done"
    }
    outputs.upToDateWhen { false }
}
复制代码

Whenever I run this, myTask is out-of-date. If I switch the false to true, the first time I run it, the task is out-of-date still (because the buildscript changed). When I run it again, it would be up-to-date (all inputs are the same). You'll see this at --info level logging.

Failed to notify project evaluation listener

The versions of the Android Gradle plugin and Gradle are not compatible.

参考

关注下面的标签,发现更多相似文章
评论