Gradle详解

如何学习Gradle?

Gradle详解

项目自动化构建的发展

每次编译项目的时候都是在命令行下对每个源文件执行编译命令,这种方式对于源文件不多的小项目还行,但是当项目比较大有成百上千个源文件需要编译时就比较痛苦了,所以才有了一些自动化构建工具的诞生

Makefile

最早出现的构建工具是makefile,它主要用于C/C++项目,大家会发现Android的源码里面用的就是这一套构建机制。makefile文件将程序编译,链接,装载(编译原理上的东西,不熟悉的可以去翻阅相关书籍)的流程定义成一套统一的规则。其中就包括了:哪些源文件需要编译,如何去编译,依赖的库文件,以及如何生成最终的可执行文件等等。通过这些规则去实现我们的构建需求,那么当你在编译整个工程的时候就只需要在命令行下执行一个make命令就可以搞定了,极大的提高了项目的构建效率。

ANT

ant也是一套构建工具,主要应用于Java项目(在eclipse上开发Android项目的时候用的比较多)。它是一个将软件编译,测试,部署过程组织起来自动化执行的工具。

MAVEN

ant虽然能大幅提高构建的效率,但是也存在一些缺点,比方说ant中的组件依赖(jar包)不能跨网络使用,为了解决这个问题,于是maven出现了,maven使用了强大的中央仓库,使得项目中使用到的一些公共组件可以很方便的联网依赖和更新,这也极大的方便了一些开源项目的使用。

Gradle

由于maven的配置过于复杂和繁琐,于是出现了我们今天的主角gradle,下面给大家看下两者配置文件的对比。

maven的配置:

<dependencies>
    <dependency>
        <groupId>com.crashlytics.sdk.android</groupId>
        <artifactId>crashlytics</artifactId>
        <version>2.5.5</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
    </dependency>
</dependencies>

gradle的配置

dependencies {
    compile('com.crashlytics.sdk.android:crashlytics:2.5.5@aar')
    testCompile('junit:junit:4.7')
}

Gradle介绍

Gradle 是一个基于Apache和Apache Maven概念的项目自动化构建工具。他使用一种基于Groovy的特定领域语言赖声明项目设置,而不是传统的XML。

构建(概念)

构建叫Build也好,叫make也行。

在Android开发中,要想把开发的应用交给安装到手机使用,要经过下面几个步骤。
在这里插入图片描述
不仅仅是编写代码,而是把编写的代码打包成APK,然后对APK进行测试,然后上架到应用市场,发布到用户手中,其中打包生成APK安装包流程就是构建过程

日常生活中,和构建最类似的一个场景就是做菜。输入各种食材,然后按固定的工序,最后得到一盘菜。当然,做同样一道菜,由于需求不同,做出来的东西也不尽相同。比如,宫保鸡丁这道菜,回民要求不能放大油、口淡的要求少放盐和各种油、辣不怕的男女汉子们可以要求多放辣子…总之,做菜包含固定的工序,但是对于不同条件或需求,需要做不同的处理。

一个完整的应用构建需要经理一下步骤:
在这里插入图片描述
在没有构建系统的时候,需要理解整个过程,知道每一步具体用到哪些工具和命令行,逐个并调用,并把每一步的构建产物传递给下一步,过程繁杂,每个开发人员,都必须了解,需要做的事情也大量重复。因此需要自动化构建工具。

再比如说,Android开发中,一个APP有多个版本,Release版、Debug版、Test版。甚至针对不同APP Store都有不同的版本。每次不同需求过来,都需要手动去搞,是不是特别麻烦。

Gradle安装

1.下载安装

gradle运行JVM上,因此需要安装JDK(1.8及以上)。JDK安装不再赘述。
下载 → 官网连接

配置环境变量
配置Gradle(仓库地址)

2.生成Gradle Wrapper

新建一个Android项目,点击Run按钮直接就能运行。其实就是通过项目gradle目录,包含了gradle wrapper的配置文件,使用gradle wrapper的方式不需要提前将gradle下载好,而是会根据本地的缓存情况决定是否需要联网下载gradle。

android studio project目录下的gradlew和gradlew.bat这两个文件就是用来在命令行界面执行gradle命令的,其中gradlew是在Linux或Mac系统中使用的,gradlew.bat是在Windows系统中使用的。

Gradle使用

Gradle选择了Groovy。Groovy基于Java并拓展了Java。 Java程序员可以无缝切换到使用Groovy开发程序。Groovy说白了就是把写Java程序变得像写脚本一样简单。写完就可以执行,Groovy内部会将其编译成Javaclass然后启动虚拟机来执行。当然,这些底层的渣活不需要你管。
除了可以用很灵活的语言来写构建规则外,Gradle另外一个特点就是它是一种DSL,即Domain Specific Language,领域相关语言。什么是DSL,说白了它是某个行业中的行话,行业内的人一说就知道什么意思。在Gradle中就是一些语句或者词语就代表那个意思,你如果学过Gradle一看就知道是什么,好像就是约定俗称的东西,比如sourceSets代表源文件的集合等,外,基于行话,我们甚至可以建立一个模板,使用者只要往这个模板里填必须要填的内容,Gradle就可以非常漂亮得完成工作,得到想要的东西。
所以Gradle的使用离不开下面两个基础知识:

  • Groovy,了解Groovy语言是掌握Gradle的基础
  • Gradle中的行话
    Groovy语言参考这篇文章:Groovy基础

生命周期

参见:Gradle构建过程

基本组件

Gradle中,每一个待编译的工程都叫一个Project。每一个Project在构建的时候都包含一系列的Task。比如一个Android APK的编译可能包含:Java源码编译Task、资源编译Task、JNI编译Task、lint检查Task、打包生成APK的Task、签名Task等。
一个Project到底包含多少个Task,其实是由编译脚本指定的插件决定。插件是什么呢?插件就是用来定义Task,并具体执行这些Task的东西。

Gradle它负责定义流程和规则。而具体的编译工作则是通过插件的方式来完成的。比如编译Java有Java插件,编译Groovy有Groovy插件,编译Android APP有Android APP插件,编译Android Library有Android Library插件

Task

Task就是gradle执行的最小单元。Task,顾名思义就是任务,一个Task包含若干action,action是一个闭包。

1.创建Task

使用task关键字创建Task

task myTask  // myTask是新建Task的名字
task myTask { configure closure } // 正常写法 task + 名字 + 闭包
task myType << { task action } // <<符号是doLast的缩写 这种写法Gradle 5.0版本中已经移除
/*
Task创建的时候可以指定Type,通过type:名字表达。这是什么意思呢?其实就是告诉Gradle,
这个新建的Task对象会从哪个基类Task派生。比如,Gradle本身提供了一些通用的Task,
最常见的有Copy 任务。Copy是Gradle中的一个类。当我们:task myTask(type:Copy)的时候,
创建的Task就是一个Copy Task。
*/
task myTask(type: SomeType)
task myTask(type: SomeType) { configure closure }

Task一些知识点:

  • task在创建之后都隶属于某个Project。同一个Project中任意两个task的名字不能相同。
  • task不一定要在build.gradle的最外层,可以放在任意的代码块中,只是在执行时机上有所区别。
  • 如果将task hello放到了allprojects()中执行,它会为项目中每一个project创建一个名为hello的task
      allprojects {
          task hello
      }
    
  • task的时候还可以指定一些参数。比较重要的有
    1. type:task类型(后面讲解)
    2. dependsOn:依赖task列表(后面讲解)
    3. group:task所属的组
      如果不指定组的话,常见的任务回放在所在工程的other组中
      在这里插入图片描述
      如果指定新分组,会创建一个分组名,把新建的任务添加进去。
      在这里插入图片描述
2. 为task添加Action

每个task中可以包含0到多个Action,这些Action保存在一个ArrayList成员变量中,当执行这个task时,会按照顺序依次执行所有的Action。

Task接口提供了两个相关的方法来声明task动作:doFirst(Closure)和doLast(Closure),当task被执行时,动作逻辑被定义为闭包参数被依次执行。

doFirst()方法会将Action添加到ArrayList的开头,而doLast()方法则将Action添加到ArrayList的末尾。可以有多个doFirst()和doLast(),配置task时会按照它们出现的先后顺序添加到Action列表中。

2.1 创建Task的同时添加Action

可以在创建task的同时添加Action,只需要将其添加到创建task的闭包中即可。

以下代码在创建ms1 task的时候为它添加了4个action。

task ms1 {
    doFirst {
        println("=================ms1 doFirst1=================")
    }
    doFirst {
        println("=================ms1 doFirst2=================")
    }
    doLast {
        println("=================ms1 doLast1=================")
    }
    doLast {
        println("=================ms1 doLast2=================")
    }
}

-- 执行这个task会产生如下输出 --
=================ms1 doFirst2=================
=================ms1 doFirst1=================
=================ms1 doLast1=================
=================ms1 doLast2=================

2.2 为现有task增加Action

如下代码为build task增加了两个Action,如果其他没有对build task再做修改,那么这里添加的doFirst会作为build task的第一个Action最先被执行,而doLast则会作为build task的最后一个Action,在其他Action执行完成后再执行。

build {
    doFirst {
        println("=================build doFirst=================")
    }
    
    doLast {
        println("=================build doLast=================")
    }
}

如下代码为ms1 task添加了一个新的action,这个action被添加在了其他action的前面。

ms1.doFirst {
    println("=================ms1 doFirst=================")
}
2.3 为动态创建的task增加Action

有些task是动态创建的,无法直接在build.gradle的最外层为其增加Action,又或者需要在task定义代码之前为其添加Action,这时可以将其放到Project.afterEvaluate()方法中去执行。

afterEvaluate {
	assembleDebug {
	    doLast {
	        println("=================assembleDebug doLast=================")
	    }
	}
}

或者可以在Task creation事件的回调中去添加。

tasks.whenTaskAdded { task ->
    if (task.name == 'assembleDebug') {
      task.doFirst {
        println("=====================assembleDebug doFirst=========================")
      }
    }
}

添加action可以出现在代码任意地方,只要拿到对应的task就可以为其添加action。需要注意的是不能为正在运行的task添加action,否则会抛出异常。例如如下代码为myTask添加了一个action,在这个action执行的时候再为myTask添加另一个action,这时就会有运行时异常,但可以在一个task执行的时候为其他task添加action,这里将myTask改成任意其他的task名字是可以正常运行的。

myTask.doFirst {
    println("=================ms1 doFirst=================")
    myTask.doFirst {
        println("=================add another action=================")
   }
}

3. 添加task之间的依赖

task提供了dependsOn,finalizedBy方法来管理task之间的依赖关系,依赖关系表达的是执行这个task时所需要依赖的其他task,也就是说这个task不能被单独执行,执行这个task之前或之后需要执行另外的task。

3.1 在一个task前执行另一个task

要在一个task之前执行另一个task,可以通过配置dependsOn的依赖关系实现。

例如有两个task:taskA和taskB,通过指定taskA.dependsOn taskB就可以让在执行taskA之前先执行taskB,或者指定taskB.dependsOn taskA就可以让在执行taskB之前先执行taskA。

task taskC{
    doLast{
        println "taskC"
    }

}

task taskA{
    doLast{
        println "taskA"
    }
}

// 方式一:在创建Task的时候,设置其依赖的task
//task taskB(dependsOn: [taskA,taskC]){
//    println "taskB"
//}

task taskB{
    doLast{
        println "taskB"
    }
}

//方式二:通过调用对象方法创建依赖关系
taskB.dependsOn(taskA,taskC))
3.2 在一个task后执行另一个task

要在一个task之后执行另一个task,无法通过配置dependsOn实现。要实现这个功能需要通过finalizedBy来实现。

同样的有两个task:taskA和taskB,通过指定taskA.finalizedBy taskB就可以让在执行taskA之后执行taskB,或者指定taskB.finalizedBy taskA就可以让在执行taskB之后先执行taskA。

// 任务B之后执行任务A和任务C
taskB.finalizedBy(taskA,taskC)
4. tasks队列

gradle在执行一组task的时候会根据他们的依赖关系生成一个task序列,然后按照序列的先后顺序来依次执行各个task。单个task的执行一定是一个原子过程。

在通过dependsOn和finalizedBy设定好task之间的依赖关系后,在执行一个task时就会根据依赖关系生成一个task队列。

例如,有四个task,task0,task1,task2,task3。指定依赖关系如下。

task0 dependsOn task1
task1 dependsOn task2
task0 finalizedBy task3

task task0{
    doLast {
        println "test0"
    }

}

task task1{
    doLast {
        println "test1"
    }
}

task task2{
    doLast {
        println "test2"
    }
}

task task3{
    doLast {
        println "test3"
    }
}

task0.dependsOn task1
task1.dependsOn task2
task0.finalizedBy task3

当执行gradlew task0时就会先生成 task2 -> task1 -> task0 -> task3这样一个序列,然后按照顺序来执行

4.1 给tasks排序

在上述例子中,执行task0得到的tasks队列中每个task的位置都是明确的,它们的先后顺序也是完全确定的。然而更多的情况是,一个任务队列中有部分task的位置不是很明确。

例如,有四个task,task0,task1,task2,task3。指定依赖关系如下。

task0 dependsOn task1
task0 dependsOn task2
task0 finalizedBy task3

按照依赖关系,执行task0生成的任务队列可以是task2 -> task1 -> task0 -> task3,也可以是task1 -> task2 -> task0 -> task3,可以看到task2和task1之间的顺序是不确定的。虽然gradle总是可以按照一定的规则(例如task名字的字典序)来得到一个固定的task队列,但有时我们需要人为明确这个顺序,例如这里我们希望task2总是在task1之前执行。这时有两种方案,一种是使用dependsOn,我们让task1 dependsOn task2,这样task2一定会在task1之前执行,但这种方案会让单独执行task1的时候也会先执行task2,这有可能和实际需要不符。这时就需要使用另一种方案,通过shouldRunAfter和mustRunAfter来指定这个顺序。


task0.dependsOn task2
task0.dependsOn task1
task0.finalizedBy task3
task1.shouldRunAfter task2

shouldRunAfter和mustRunAfter表达的都是一个任务需要在另一个任务之后执行,它们的区别是mustRunAfter要求gradle生成任务队列时必须确保这个顺序一定要满足,如果不能满足就会报错,shouldRunAfter相当于建议gradle按照这顺序来生成任务队列,gradle会优先参考其他约束条件,如果在满足其他约束条件后,这条约束也能满足,那么就采纳这个顺序,如果不能满足,就忽略这个请求。

例如有依赖关系task0 dependsOn task1,这时又指定task1 mustRunAfter task0,显然这时这个条件无法被满足,因此执行task0的时候会报错。但如果指定task1 shouldRunAfter task0,则不会有错误,gradle会自动忽略掉这条请求。

再来看上述例子
task0 dependsOn task1
task0 dependsOn task2
task0 finalizedBy task3

这时无论指定task1 shouldRunAfter task2,还是task1 mustRunAfter task2,都会得到任务队列task2 -> task1 -> task0 -> task3。

需要注意的是,无论是shouldRunAfter还是mustRunAfter,影响的只是task在队列中的顺序,并不影响任何任务间的执行依赖,也就是说使用shouldRunAfter和mustRunAfter并不会导致任务队列中添加新的task。

例如指定ms1 mustRunAfter ms2,如果ms1和ms2没有其他依赖关系,那么在执行ms1的时候并不会先执行ms2,执行ms2之后也不会执行ms1

5. 预定义的gradle task

gradle为每个工程提供了很多预先定义好的task,通过这些task我们可以不需要手动创建任何task就可以实现编译,打包,测试等功能。经常使用的clean build等就是gradle中预定义的task。在做Android开发时,Android Gradle插件也提供了一组预定义的gradle task,当我们新建一个Android工程就能看到如下所示的task列表,这里的tasks都是gradle和Android Gradle Plugin为我们创建好的,可以直接使用。
预定义的Gradle task

5.1 清理工程(clean task)

clean task用来清理上次编译的缓存内容,会删除生成的build目录。

5.2 编译相关task

和编译相关的task主要有:build和assemble,其中build依赖assemble,也就是说执行build之前会先执行assemble。在Android上,会根据buildType和productFlavor的不同自动创建多个assembleXxx任务,如assembleDebug,assembleRelease等,assemble会依赖所有的assembleXxx任务,也就是说执行assemble会先执行assembleDebug,assembleRelease等一系列的assemble任务。

6. 在编译完成后执行一段代码

有时我们需要在编译任务完成后执行某一个任务,按照通常的做法,我们创建一个task,然后将这个task放到编译的task之后执行。

如前所述要在一个task之后执行另一个task,可以通过finalizedBy来实现,但是编译任务对应的task很多,有些还是动态创建的,如果每个任务都指定一遍finalizedBy会很麻烦。好在gradle提供了一个buidlFinished()方法,它可以在编译完成后执行一定的操作,不用理会当前执行的是哪个编译task,只需要把task中代码移到buidlFinished中即可。

project.gradle.buildFinished {
    println "build finished"
}

7. task相关属性
7.1 获取当前task的名字

要在当前任务里获取自己的名字,可以使用其name属性

task ms1{
    doLast {
        println name			// ms1
    }
}
7.2 配置任务属性

可以在任务配置代码中,通过ext.xxx来为任务添加自定义属性。

task myTask {
    ext.myProperty = "myValue"
}

8. 默认任务

Gradle 允许在build.gradle脚本中定义一个或多个默认任务,默认任务的意思是没有指定任何任务的时候会执行默认任务。要指定默认任务可以使用defaultTasks关键字。

例如,如下代码指定默认任务为myTask和build,则执行gradlew时如果没有指定任何任务,就会执行myTask和build。

defaultTasks 'myTask', 'build'

task myTask {
    doLast {
        println "run default task."
    }
}

需要注意的是,使用defaultTasks指定默认任务的时候,任务名一定要加引号,即使它是在任务定义之后声明,也是如此。

9. 覆盖已有的任务(了解)

有时我们可能需要覆盖掉一个已有的任务,这个任务可能是自定义的,或者是来自其他Gradle插件中的任务。虽然这种场景并不常见,不过还是有必要了解这种用法,说不定在需要的时候就能帮上大忙。

覆盖已有任务只需要重新创建一个新的同名task,并指定overwrite为true即可。

overwrite的覆盖操作,不仅会覆盖掉原先task的所有action,而且会覆盖原先task指定的type类型,还会覆盖掉所有和原先task相关的依赖关系。被覆盖的依赖关系不仅包含原先的task对其他task的依赖,还包含其他task对原先task的依赖,所有的和原先task相关的依赖关系都会被移除。当然,可以在overwrite之后重新定义新的依赖关系。

overwrite的覆盖操作不会覆盖原先创建task块中的代码,所有创建task代码仍然会被执行。

此外,如果定义overwrite的task在这之前并没有被创建,那么gradle会忽略这个属性,等同于创建一个新的task,不会有错误出现。

如下代码创建了两个名为copy的task,第二个copy task指定了overwrite为true,因此第二个copy task会覆盖第一个。此外,第一个copy task依赖一个名为delete的task,由于这条依赖关系在overwrite之前配置,所以同样会被覆盖。

task delete(type: Delete) {
    delete "s1.txt"
}

task copy(type: Copy) {
    println name
    from(file('ss.txt'))
    into(file('new_dir'))
    doLast {
        println "I am a copy task"
    }
}

copy.dependsOn delete

task copy(overwrite: true) {
    println name
    doLast {
        println('I am the new one.')
    }
}

这里执行gradlew copy的完整流程如下。

  1. 创建delete task,执行语句delete “s1.txt”,注意这里只是执行了delete()方法指定了要删除的文件,并没有真正执行删除操作,删除操作只有执行这个task的时候,才会在delete task的action中执行。
  2. 创建第一个copy task,依次执行所有代码,即println name, from(file(‘ss.txt’)),into(file(‘new_dir’))和doLast。同样的这里的doLast只是为task添加了action,并不是真正的执行了这个action。
  3. 执行copy.dependsOn delete为copy添加一个依赖关系。
  4. 创建第二个copy task,由于overwrite为true,所以会移除原先的copy task,原先copy task所有信息都会丢失,包括依赖关系,包括指定的type,也包括 from(file(‘ss.txt’)),into(file(‘new_dir’))的配置和doLast添加的action。但是println name由于已经执行过了,已经打印出来的内容不会消失不见。然后重新创建新的copy task,执行println name和doLast。
  5. 执行copy task,由于原先task已经不存在,所以只会执行println(‘I am the new one.’)这个action中的语句。不会执行println “I am a copy task”,也不会执行复制’ss.txt’到new_dir的操作,也不会执行delete task。最终结果就是打印了两次name和一次I am a copy task。

Gradle 插件

在gradle世界中,是一个构建工具,让工程构建更自动化,不过,他只是一个运行环境,提供了基本的框架,而真正的构建行为不是它提供的,Gradle负责在运行的时候,找到所有需要执行的任务,挨个去执行,而任务是由谁来提供的呢?

  • 自定义任务
  • 插件

gradle中几乎所有的功能,比如编译代码等都是以插件的形式去提供的。插件负责封装,并且提供gradle运行期间需要的task,在工程中依赖某个插件之后,就能复用插件提供的构建行为。

Gradle内置了很多的语言插件,基本上能够满足大部分的构建工作,但有些插件没有内置,或者某些功能没有提供,就可以通过自定义插件或者引入其他插件解决。比如Android gradle插件就是通过java插件去拓展的。它在编译Java代码的基础上,还添加了编译资源,打包APK等功能。

插件类型
  • 二进制插件
    二进制插件通常实现了Plugin接口
  • 脚本插件
    独立的gradle脚本。
插件使用
  1. 声明与下载插件在这里插入图片描述
  2. 加载插件:插件与工程绑定
    加载插件是掉用它的apply函数,
    函数调用的时候通过 参数名1:参数值2,参数名2:参数值2 的方式来传递参数
    apply支持三种参数:
  • from:使用其他脚本,值可以为 Project.uri(Object) 支持的路径
  • plugin:使用其他插件,值可以为插件id或者是插件的具体实现类
  • to:
    在这里插入图片描述
// 引入二进制插件
apply plugin: 'com.android.library'   // <==如果是编译Library,则加载此插件
apply plugin: 'com.android.application' // <==如果是编译Android APP,则加载此插件

// 引入脚本插件
apply from: 脚本插件的绝对路径
apply from: rootProject.getRootDir().getAbsolutePath() + "/utils.gradle"
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值