第十一章 gradle自动化构建系列文章 之 gradle中的task

第十一章 gradle自动化构建系列文章 之 gradle中的task

< center>

查看 “Android自动化构建系列” 全部文章

一、 task的定义和配置

1. 查看当前工程下的所有task

命令:

gradle tasks

运行结果:

2. task创建

创建第一个task ,可以以输出 hellotask为例子,这里总结几种常用的任务创建方式

2.1 调用Project的task()方法创建Task

// 调用Project的task关键字创建 Task
task helloTask{
    println "hello task"
}

运行结果:

他还有一种变形

// 这里的 << 表示追加的意思, 向hello1 task中追加执行过程
task helloTask<<{
    println "hello task"
}




//上面的代码是和下面之中代码是等价的(后面详细讲解)
task helloTask {
	doLast{
	 	println "hello task"
	}
}




//也可以这样写

task helloTask {
	println "我会执行在配置阶段"
}

helloTask.doLast{
	 println "我会执行在执行阶段"
}

//输出结果
我会执行在配置阶段
我会执行在执行阶段

在上面的几个例子中,Gradle的DSL向我们展示了一种非常自然的风格来创建Task,而事实上这些都只是一种内部DSL,也即必须符合groovy的语法要求。

上面的task关键字实际上是一个方法调用,该方法属于Project。Project中存在多个重载的task()方法。和Ruby等动态语言一样,在调用groovy方法时,我们不用将参数放在括号里面。

以上我们自定义的几个Task都位于TaskContainer中,Project中的tasks属性即表示该TaskContainer。为此,我们可以新建一个Task来显示这些信息:

	task printlnAllTask<<{
	    tasks.eachWithIndex {task,index->
	        println "${index}     ${task.name}"
	    }
	}

执行结果:

0     androidDependencies
1     assemble
2     assembleAndroidTest
3     assembleDebug

	...

94     helloTask
95     outputApp1
96     outputApp2
	...
128     signingReport
129     sourceSets
130     test
131     testDebugUnitTest
132     testReleaseUnitTest
	...
154     uninstallAll
155     uninstallDebugAndroidTest
156     validateSigningDebugAndroidTest
157     verifyReleaseResources

2.2 通过TaskContainer的create()方法创建Task

在上文中我们讲到,通过task()方法创建的Task都被存放在了TaskContainer中,而Project又维护了一个TaskContainer类型的属性tasks,那么我们完全可以直接向TaskContainer里面添加Task。

通过TaskContainer的API文档可以发现,TaskContainer向我们提供了大量重载的create()方法用于添加Task。

// 通过TaskContainer 方式创建Task
tasks.create(name: 'hello4') << {
    println 'hello4'
}

运行结果:

3. 配置Task

一个Task除了执行操作之外,还可以包含多个Property,其中有Gradle为每个Task默认定义的Property,比如description,logger等。

另外,每一个特定的Task类型还可以含有特定的Property,比如Copy的from和to等。

当然,我们还可以动态地向Task中加入额外的Property。在执行一个Task之前,我们通常都需要先设定Property的值,Gradle提供了多种方法设置Task的Property值。

  • 查看源码可以得到所有可以配置的变量
属性名称 属性描述
group task 所在的分组,所有的task将按照这个分组归类,方便我们以后的查看
description 对当前task的描述,方便阅读
name 指定当前task的名称
type 指定当前task的类型
dependsOn 指定当前task与其他task之间的依赖关系
overwirte 重写执行task
action 为task配置执行的逻辑

创建时候配置

//group : task 所在的分组,所有的task将按照这个分组归类,方便我们以后的查看 ;description : 对当前task的描述,方便阅读
task helloTask(group:"study",description:"task 创建学习"){
    
    println "hello task"

    doFirst{
        println "doFirst 1"
    }
    doFirst{
        println "doFirst 2"
    }
    doLast{
        println "doLast 1"
    }
    doLast{
        println "doLast 2"
    }
}

	
	
	helloTask.doFirst{
	    println "doFirst 3"
	}
	
	
	helloTask.doLast{
	    println "doLast 3"
	}

看看所有的task:

闭包中进行配置

	task helloTask {
	    setGroup("Study")
	    setDescription("创建学习")
	    println "hello task"
	
	    doFirst {
	        println "doFirst 1"
	    }
	    doFirst {
	        println "doFirst 2"
	    }
	    doLast {
	        println "doLast 1"
	    }
	    doLast {
	        println "doLast 2"
	    }
	}
	
	
	helloTask.doFirst {
	    println "doFirst 3"
	}
	
	
	helloTask.doLast {
	    println "doLast 3"
	}

查看所有的task 同样会得如下结果:

注意task属性的配置一定要在任务执行前

4. task的执行

gradle的声明周期有三个阶段,分别是: 初始化阶段,配置阶段,执行阶段 三个阶段

  • 初始化对应groovy中的初始化阶段,主要是读取setting文件,获取所有的projec和task

  • 配置阶段主要根据依赖关系组成有线无环图,同时完成task中变量的初始化,方法的执行(简单理解:创建的任务没有放在doFirst和doLast中的代码全部都会在配置阶段执行)

  • 执行阶段,定义在doFirst和doLast 中代码执行在构建后的执行阶段

      task helloTask {
         setGroup("study")
          setDescription("创建学习")
          println "我执行在 hello task 的配置阶段"
      
          doFirst {
              println "doFirst 1     我执行在 hello task 已有的的task之前"
          }
          doFirst {
              println "doFirst 2     我执行在 hello task 已有的task之前"
          }
          doLast {
              println "doLast 1   我执行在 hello task 已有的task之后"
          }
          doLast {
              println "doLast 2   我执行在 hello task 已有的task之后"
          }
      }
      
      
      helloTask.doFirst {
          println "doFirst 3      我执行在 hello task 已有的task之前"
      }
      
      
      helloTask.doLast {
          println "doLast 3       我执行在 hello task 已有的task之后"
      }
    

执行结果:

综上可见 doFirst后添加的先执行,先添加的后执行;doLast 后添加的后执行

5. 实践统计打包时长

//计算build执行时长
def buildStartTime, buildEndTime
this.afterEvaluate { Project project ->
    //保证要找到的task执行完毕
    def proBuildTask = project.tasks.getByName("preBuild")
    proBuildTask.doFirst {
        buildStartTime=System.currentTimeMillis()
        println "build start time is ${buildStartTime}"
    }
    proBuildTask.doLast {
        buildEndTime=System.currentTimeMillis()
        println "build end time is ${buildEndTime}"

        println "the build used time is ${buildEndTime-buildStartTime}"
    }
}

执行结果:

上面案例中的知识点比较多:

1.在配置阶段直接获取子工程的任务有很多大几率会不成功(理论上不会成功),配置阶段子类还没有执行配置,无法获取到当前的任务和配置

在配置完成的监听 afterEvaluate 中是可以做到的

doFirst方法可以在任务的执行开始往前追加一个事件

doLast 方法可以在任务执行的最后补加一个事件

6.获取任务

TaskContainer 本身是一个 set 集合,是一个维护和管理了所有task的集合,可以通过这个集合查找对应的task

6.1 通过tasks.findbypath查找,如果找不到会返回null

在APP project中定义task printName

task printName{
    doFirst{
        println "在APP project 中打印一句话"
    }
}

在rootProject中定义aaa

task aaa {
    
    println "___________________________${this.tasks.findByPath(":app:printName").name}"
    doLast{
        println "___________________________${this.tasks.findByPath(":app:printName").name}"

        print "当前获取到的compileSdkVersion属性是:${this.ext.android.compileSdkVersion}"
    }
}

运行结果:

6.2 通过tasks.getByPath查找,如果找不到会报错UnknownTaskException

在APP project中定义task printName

task printName{
    doFirst{
        println "在APP project 中打印一句话"
    }
}

在rootProject中定义aaa

task aaa {
    
    //println "___________________________${this.tasks.getByPath(":app:printName").name}"
    doLast{
        println "___________________________${this.tasks.getByPath(":app:printName").name}"

        print "当前获取到的compileSdkVersion属性是:${this.ext.android.compileSdkVersion}"
    }
}

运行结果:

将APP中的 printName task去掉重新运行

直接抛出异常

二、task的执行顺序

首先了解下,控制task执行顺序的方式有两种

  • 1.通过dependsOn强依赖的方式

  • 2.通过api指定执行顺序

1. 使用 dependsOn 强依赖的方式控制task的执行顺序

1.1 定义task的同时传入denpendsOn属性

在rootProject中定义几个简单的task

task taskX{
    doLast{
        println "taskX"
    }
}



task taskY{
    doLast{
        println "taskY"
    }
}



task taskZ(dependsOn:[taskX,taskY]){
    doLast{
        println "taskZ"
    }
}

运行结果

1.2 在task内部修改denpendsOn属性

task taskX{
    doLast{
        println "taskX"
    }
}



task taskY{
    doLast{
        println "taskY"
    }
}



task taskZ {
    setDependsOn([taskX, taskY])
    doLast {
        println "taskZ"
    }
}

运行结果

结论:

  • taskX和taskY都是优先在taskZ前面执行的,但是taskX和taskY的执行顺序是无法确定的或者说是不可控的

  • task的执行事先从被依赖的他task开始执行的

  • 被依赖的task一定放在前面定义否则会找不到,报错Could not get unknown property 'taskY' for root project 'ElecticPowerProjectRepair' of type org.gradle.api.Project.,报错和明显是依赖了还没有初始化的task任务导致的,gradle类似其他脚本,代码的初始化时自上往下配置的,定义在同一个文件前面的代码会优先执行,既然知道了原因,那解决办法就简单了,可以通过监听初始化生命周期来配置:

      task taskX {
          doLast {
              println "taskX"
          }
      }
      
      
      
      
      task taskZ {
      	//这里需要找到对应project对他的生命周期进行监听
          this.afterEvaluate {
              setDependsOn([taskX, taskY])
          }
          doLast {
              println "taskZ"
          }
      }
      
      
      
      
      task taskY {
          doLast {
              println "taskY"
          }
      }
    

运行结果

1.3 对于无法改变源码的task,可以找到这个task,外部配置她的dependsOn属性来改变依赖,改变时最好先获取这个task原有的依赖,将我们需要的依赖插入到原因原有依赖的后面,防止覆盖task原有的依赖

task taskX{
    doLast{
        println "taskX"
    }
}



task taskY{
    doLast{
        println "taskY"
    }
}



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


tasks.findByPath("taskZ").dependsOn([taskX,taskY])
println "依赖配置完成"

运行结果:

这种使用的场景比较明确,当我们自己编写了一个task,这个task需要完成的一部分工作已经有现成task可以达到这个效果,我们可以通过依赖系统指定已经封装完成的现有task或者第三方库中的task

2. 通过api指定依赖关系

  • api关键字

| 关键字名称 | 作用 | 备注 |使用方法|
|-------------------|---------------|------------|
| mustRunAfter | 指定一个任务必须执行在另一个任务之后 |优先级小于dependsOn属性|taskA.mustRunAfter taskB|
| shouldRunAfter | 指定一个任务应该执行在另一个任务之后 |一般不用|

代码测试,为了方便构建一个taskTest辅助执行

	task taskX {
	    doLast {
	        println "taskX"
	    }
	}
	
	
	
	
	
	task taskZ {
	    
	    doLast {
	        println "taskZ"
	    }
	}
	
	task taskY {
	    doLast {
	        println "taskY"
	    }
	}
	
	taskZ.mustRunAfter([taskX,taskY])
	
	task taskTest{
	     setDependsOn([taskZ,taskX,taskY])
	}

运行结果:

经典案例

任务依赖在实际开发中的应该用,最近公司有个需求,每次发布版本的时候需要打包,并将最新的版本包和对应的日志信息文件备份到指定目录。随后调用上传任务将apk文件上传到公司的线上服务器

对于上面的小案例我们可以轻松的设计出自己的task流程图,

1.3 案例实战android应用发版版本记录

实战背景: 公司一般会要求我们的apk在发版时,对本次的版本信息做备份记录,并且对于历史的APP版本,我们应该有一个合理的树形结构方便的查看各个版本的变化和维护的内容

最终的输出实现:

功能实现代码

1.在rootProject中准备一个文件夹output并在文件夹中创建文件 releasesMsg.xml,文件中的内容同上图

2.在rootProject的build文件中创建task

	/**
	 * 生成对应版本的版本信息文件
	 */
	task handleReleaseTask( group: "releaseApp", description: "生成版本历史记录信息文件,方便查看每个版本的变更记录") {
	    inputs.file logFile
	    outputs.file versionFile
	
	    doLast {
	        def srcFile = inputs.files.singleFile
	        def desFile = outputs.files.singleFile
	
	        if (!desFile.exists()) {
	            desFile.mkdirs()
	        }
	        def releases = new XmlParser().parse(srcFile)
	
	        releases.each { release ->
	            def versionCode = release.versionCode.text()
	            def versionName = release.versionName.text()
	            def versionInfo = release.versionInfo.text()
	            println "versionCode: ${versionCode}   versionName: ${versionName} versionInfo: ${versionInfo}"
	
	            def outFile = new File("${desFile.path}\\release-${versionName}.properties")
	            outFile.withWriter { writer ->
	                writer.write("versionCode: ${versionCode}   versionName: ${versionName} versionInfo: ${versionInfo}")
	            }
	
	        }
	    }
	}

3. 执行代码,得到运行结果

4.文件中的内容

三、task的输入输出

什么是task的输入输出: 加单说就是多个task上一个task的执行结果或者执行产物作为下一个task执行的参数(或者说是传入值)

这样做的意义是什么?增量式构建,简单说就是避免重复编译,节约构建时间,感兴趣的同学可以查看gradle构建系列文章

如何进行输入输出

	//task的inputs和outputs属性
	task writeBuildLogTask(group: "releaseApp", description: "将版本信息记录到指定的历史版本日志中") {
		
		
		    inputs.properties([versionCode: versionCode_,
		                       versionName: versionName_,
		                       versionInfo: versionInfo_])
		    outputs.file logFile

			doLast{
				...
			}
	}

通过查看task源码发现

task的内部其实是维护有一个默认创建的inputs和outputs的

进一步我们看一下这个inputs和outputs所支持的类型

  • 首先是intputs的类型,简单说就是文件、键值对、map集合几种

  • 然后看一下outputs输出类型,只有file一种

上面我们默认有了一个releasesMsg.xml文件在rootProject下的output文件夹下,下面我们以输入输出的方式完成以下步骤

    1. 在rootProject定义扩展属性,当然最好是将他们提取到一个单独的文件中

         ext {
             versionCode_ = 4
             versionName_ = "1.0.7"
             versionInfo_ = "App 第四个版本,对一直功能进行优化"
             println "---------------${rootProject.getProjectDir().path}"
             logFile = new File("${rootProject.getProjectDir()}\\output\\releasesMsg.xml")
             if (!logFile.getParentFile().exists()) {
                 logFile.getParentFile().mkdirs()
             }
         }
      
    1. 对应 writeBuildLogTask 完成将版本信息书写到 releasesMsg.xml 文件中

         task writeBuildLogTask(group: "releaseApp", description: "将版本信息记录到指定的历史版本日志中") {
         
         
             inputs.properties([versionCode: versionCode_,
                                versionName: versionName_,
                                versionInfo: versionInfo_])
             outputs.file logFile
         
             doFirst {
                
                 def properties = inputs.getProperties()
                 File log = outputs.getFiles().getSingleFile()
                 //将版本信息转换成实体类
                 def versionMsgBean = new versionMsg(properties)
                 def sw = new StringWriter()
                 def builder = new MarkupBuilder(sw)
         
         
         
                 if (!log.exists()) {
                     log.createNewFile()
                 }
                 println "+++++++++++++++++"
                 if (log.text != null) {
                     //文件中没有数据
                     if (log.text.length() <= 0) {
                         println "----------"
                         builder.releases {
                             relase {
                                 versionCode(properties.versionCode)
                                 versionName(properties.versionName)
                                 versionInfo(properties.versionInfo)
                             }
                         }
                         log.withWriter("utf-8") { writer ->
                             writer.write(sw.toString())
                         }
                     } else {
                         builder.release {
                             versionCode(properties.versionCode)
                             versionName(properties.versionName)
                             versionInfo(properties.versionInfo)
                         }
                        
                         def lines = log.readLines()
                         def length = lines.size() - 1
                         def sBuffer = new StringBuffer()
                         lines.eachWithIndex { line, index ->
                             if (index < length) {
                                 sBuffer.append(line).append("\r\n")
                             } else {
                                 sBuffer.append(sw.toString()).append("\r\n").append(line)
                             }
                             println line
                         }
                         log.withWriter("utf-8") { writer ->
                             writer.write(sBuffer.toString())
                         }
                     }
                 } else {
                     println "log's text is null"
                 }
             }
         }
      

完成后生成如图文件:

    1. 读取历史版本文件生成每个版本的版本信息文件,对应task

        /**
         * 生成对应版本的版本信息文件
         */
        task handleReleaseTask( group: "releaseApp", description: "生成版本历史记录信息文件,方便查看每个版本的变更记录") {
            inputs.file logFile
            outputs.file versionFile
        
            doLast {
                def srcFile = inputs.files.singleFile
                def desFile = outputs.files.singleFile
        
                if (!desFile.exists()) {
                    desFile.mkdirs()
                }
                def releases = new XmlParser().parse(srcFile)
        
                releases.each { release ->
                    def versionCode = release.versionCode.text()
                    def versionName = release.versionName.text()
                    def versionInfo = release.versionInfo.text()
                    println "versionCode: ${versionCode}   versionName: ${versionName} versionInfo: ${versionInfo}"
        
                    def outFile = new File("${desFile.path}\\release-${versionName}.properties")
                    outFile.withWriter { writer ->
                        writer.write("versionCode: ${versionCode}   versionName: ${versionName} versionInfo: ${versionInfo}")
                    }
        
                }
            }
        }
      
    1. 添加依赖关系

         handleReleaseTask.mustRunAfter writeBuildLogTask
      
    1. 构建一个辅助执行的 testTask 目的方便我们这两个task的执行

      task testTask {
      dependsOn writeBuildLogTask, handleReleaseTask
      doFirst {
      println “---------程序构建完成--------”
      }
      }

    1. 执行后的结果:

####总结: ####

上面的构建例子包含了很多详细的知识点

1. 到我们task指定了输入输出的时候,如果输入量 (inputs) 和输出量 (outputs) 都没有改变时这个task的配置代码部分将会执行,但是这个代码的action部分将不会被执行(doFirst和doLast),这时候我们在执行task时对应的task将会被标记为

1.同样是上面的这个例子,执行testTask,第一次执行在控制台上输出的结果是

2.第二次执行的结果是

很明显,增加了一行输出,Task :handleReleaseTask UP-TO-DATE ,这个表明我们在第二次执行上面的这个task的时候,他得inputs和outputs变量都没有改变和第一次执行时的输入量相同并且输出文件的结果完全一致,这时候系统将会认为这个task没有必要再次执行(其实是gradle语言的优化机制增量构建),如果我们想让这个gradle的action再次执行我们应该怎么办呢,很简单,修改输入参数的值(文件的话就修改这个文件的内容即可)
这里修改一下输出文件的内容,加一个空格输出结果变成了

很明显,action在此被执行了
其实系统很多的构建都是采用这种方式进行增量的,在我们以后的开发中可以多学习使用这种方式,减少构建消耗的时间。

四. 将自定义的task挂在到其他task的生命周期当中

/**
 * 监听gradle配置阶段执行完成,可以抱枕我们可以获取到project下的所有task
 */
rootProject.afterEvaluate { project ->
    println "------------${project.name}"
    def taskClean = project.tasks.findByPath("clean")
    println "${taskClean.name}"
    if(taskClean==null){
        throw new GradleException("the task clean is not found.")
    }
    //将testTask追加在clean的后面
    taskClean.doLast {
        taskZ.execute()
        println "---------taskClean.doLast --------------------"
    }
}





	
task taskZ {
    doLast {
        println "taskZ"
    }
}

和心部分就是监听任务配置完成,找到指定的task,然后在其action中追加我们自己的task

执行结果:

执行成功

但是执行taskTest这种带有依赖的task会和我们想像的结果存在出入

task taskTest {
    setDependsOn([taskZ, taskX, taskY])
    doLast{
        println "------------taskTest-----------"
    }
}

执行结果为:

依赖的taskZ, taskX, taskY三个task并没有执行。

明白了挂在的使用,我们可以很容易的将上面生成版本的task挂在到工程的构建过程build中去

	/**
	 * 监听gradle配置阶段执行完成,可以抱枕我们可以获取到project下的所有task
	 */
	rootProject.afterEvaluate { project ->
	    println "------------${project.name}"
	    def taskClean = project.tasks.findByPath("clean")
	    println "${taskClean.name}"
	    if (taskClean == null) {
	        throw new GradleException("the task clean is not found.")
	    }
	    //将testTask追加在clean的后面
	    taskClean.doLast {
	        writeBuildLogTask.execute()
	        handleReleaseTask.execute()
	        println "---------taskClean.doLast --------------------"
	    }
	}

运行结果,很明显成功了。但是dependsOn 和 must属性配置的task执行顺序没有了,execute()的执行优先级将会更高,所有交换两个task的位置将会报错

五. task的依赖关系查看

请查看我的gradle中task 依赖关系查看这篇文章

查看 “Android自动化构建系列” 全部文章

你可能感兴趣的:(gradle构建工具系列)