Gradle编译的那些事——从Groovy到Gradle插件开发

1.Android项目中的一些build.gradle分析

apply plugin: 'com.android.application'
apply from: 'test.gradle'
 
allprojects {
    repositories {
        google()
        jcenter()
    }
}

最初的疑问

想这几句gradle的设置是什么含义?其中apply是什么,做了什么?

allprojects、repositories这些语句设置了什么,为什么这样写?

apply实际上时一个方法,它接收一个闭包作为参数,参数plugin:"java"实际上是一个map的结构,键值对。

repository这段代码实际上也是一个方法的调用,参数是一段闭包。这是gradle编译脚本中的特殊语法。

我们可以查看一下build.gradle脚本的代码:

 

 

从源码可以看到: 

void allprojects(groovy.lang.Closure closure);
void afterEvaluate(groovy.lang.Closure closure);
void dependencies(groovy.lang.Closure closure);
void buildscript(groovy.lang.Closure closure);

在Project这个接口类中定义了很多的方法,比如allprojects,他们几乎都拥有一个Closure类型的参数。

刚开始接触到gradle时候,一直以为这个gradel.build文件就是这样的固定写法。后来了解了一些编译的知识才明白,gradle文件也是一种特定语言编写的的文件。而这种语言不常见,而Closure就是它的特殊定义之一。

Closure是什么类型?这里先要了解一些gradle默认编写的语言。

Groovy语言

Gradle默认是由groovy语言来编写的,了解groovy的人大概知道,groovy是一种面相对象的语言,因此和java、kotlin有很多相似的地方。

 groovy官网:The Apache Groovy programming language 

Groovy中一些特别的点

1.闭包

简单的理解,闭包就是一段由大括号括起来的代码块,它有一个类型声明:groovy.lang.Closure closure。通常我们可以定义一段代码块作为参数传递给一个参数是Closure类型的方法调用。

/**
 * 定义一个clause
 */
def ageClosure = {
    int age = 20
    while (age<100){
        age++
    }
}
/**
 * 带参数的Cosure
 */
def ageClosure = {
    param->
    int age = 20
    while (age<100){
        age++
    }
    println "$param--$age"
}
//不同的调用closure方式
def  printAge(Closure closure){
    println "==================================="
    closure.call("这是一个参数")
    println "==================================="
}
//直接作为一个方法调用
ageClosure("text")
ageClosure()
//作为参数
printAge(ageClosure)

闭包看起来和kotlin的一些语法比较像,有什么区别:

//kotlin
@Test
fun main(){
   var list = mutableListOf<String>()
   list.apply {
       add("java")
       add("kotlin")
       add("html")
       }
   list.forEach {
       println(it)
   }
}
//groovy文件中的代码
List<String> list = new ArrayList<>()
list.add("java")
list.add("kotlin")
list.add("groovy")
list.add("python")
 
/**
 * 普通groovy List的forEach方法
 */
list.forEach{
    println(it)
}

可以看到对于一些语法kotlin和groovy的使用几乎一样。groovy的闭包使用和kotlin的语法也很像:

/**
 * 闭包在方法中的使用,和kotlin语法类似
 */
def targetFile = new File("file.txt")
targetFile.eachLine {
    println(it)
}

代码从一个file.txt文件按行读取文件,然后传递一个闭包,在闭包中打印出每行字符。这里的eachLine方法和前面的kotlin的语法相似,但是参数不同:kotlin的forEach方法

public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
    for (element in this) action(element)
}

参数是一个返回Unit的代码块,而groovy的eachLine方法参数是一个Closure闭包。

2.省略

Groovy语言由于其特性,可以做一些简化的省略,比如省略方法的括号,省略参数的引号。

比如apply()方法。实际上是:

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

我们常用的println()方法,可以省略括号。

tasks.register("hi"){
    it.doLast{
        println("hellp groovy ~")
        println "hello groovy !!"
    }
}

 代码闭包的形式,再加上省略,以及固定的一些写法,可能是刚开始接触build.gradle难理解的几个原因。

2.Gradle是什么,它是如何执行的?

关于gradle官方有他的介绍:What is Gradle?

总结来说就是几个字:Gradle是高性能的、可编程的、基于JVM的、可扩展的编译框架。

1.在Gradle之前的代码编译工具Ant和maven

Ant构建项目

Ant是早期Apache开发的用于构建java项目的构建工具,它是用xml来编写编译配置文件的,内部内置了很多预定义的任务,方便Java开发着编译,部署项目。

一个Hello world Java项目使用ant 编译项目的配置文件build.xml

<?xml version = "1.0"?>
<project name = "fax" basedir = "." default = "build">
   <property name = "src.dir" value = "src"/>
   <property name = "web.dir" value = "war"/>
   <property name = "build.dir" value = "${web.dir}/WEB-INF/classes"/>
   <property name = "name" value = "fax"/>
   <path id = "master-classpath">
      <fileset dir = "${web.dir}/WEB-INF/lib">
         <include name = "*.jar"/>
      </fileset>
      <pathelement path = "${build.dir}"/>
   </path>
   <target name = "build" description = "Compile source tree java files">
      <mkdir dir = "${build.dir}"/>
      <javac destdir = "${build.dir}" source = "1.5" target = "1.5">
         <src path = "${src.dir}"/>
         <classpath refid = "master-classpath"/>
      </javac>
   </target>
   <target name = "clean" description = "Clean output directories">
      <delete>
         <fileset dir = "${build.dir}">
            <include name = "**/*.class"/>
         </fileset>
      </delete>
   </target>
</project>

其中:

src.dir指的是可以找到java源文件的项目的源文件夹

build.dir是指项目编译的输出文件夹

web.dir是指项目的Web源文件夹,您可以在其中找到JSP,web.xml,css,javascript和其他Web相关文件

有一个name是clean的target和build的target,从名字可以看出这是用来clean和build操作的。

Ant参考 Ant - 介绍_学习Apache Ant|WIKI教程 

Maven 构建项目

maven是Apache中的一个编译和管理工具,主要对Java项目进行构建和管理。maven能够帮助开发人员完成项目的构建、文档生成、依赖、发布、分发等操作。

使用命令创建一个基于maven编译的项目的pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.companyname.bank</groupId>
  <artifactId>consumerBanking</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>consumerBanking</name>
  <url>http://maven.apache.org</url>
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

从pom. xml代码中,可知 Maven 已经添加了 JUnit 作为测试框架。

执行mvn clean package 可以完成 clean 并打包生成一个jar

网上一个典型的maven构建的生命周期图:

https://www.runoob.com/wp-content/uploads/2018/09/maven-package-build-phase.png

maven中引入第三方依赖:

通过给pom.xml中添加一个dependencies节点

<dependencies>
    <!-- 在这里添加你的依赖 -->
   <dependencies>
      <dependency>
         <groupId>com.companyname.common-lib</groupId><!-- 库名称,也可以自定义 -->
         <artifactId>common-lib</artifactId><!-- 库名称,也可以自定义 -->
         <version>1.0.0</version><!--版本号-->
      </dependency>
   <dependencies>
</dependencies>

maven 参考 Maven 教程 | 菜鸟教程 

Ant、maven、Gradle的区别和联系

Ant是最早的构建工具之一,它可以定义不同的task来处理任务,然后执行命令完成编译。但是用户发现ant有一个很大的缺陷,不能管理依赖,对于一些第三方的库,在使用的时候都需要手动拷贝到lib目录,这个操作很烦且容易出错。

后来为了解决依赖管理的问题,Apache提出了maven,maven最大的改进在于提出了仓库的概念。开发者可以把所有的依赖都放到仓库中,在项目的管理文件中,我们只需要标注需要什么包,什么版本,maven就自动去对一个的仓库寻找,并且打包到项目中来。对应了pom.xml文件中dependency节点。maven改进了ant的target,增加了编译的生命周期等一些新概念。

maven已经可以满足大部分工程的构建,但是由于maven也是xml文件配置的,语法不够简单,不能实现随意的编程设置,并且maven不支持自定义任务,只能以插件的方式工作。Gradle在maven基础上出现了,它充分利用了maven的资源,继承了maven的中央仓库。比起xml的写法,gradle使用面相对象的语言来编程脚本,可以像编写其他程序一样来编写编译工具。后来有一些开发工具开始集成gradle作为编译工具,更进一步简化了gradle脚本的使用,创建项目会帮开发者自动创建基本的脚本代码。而我们如果想添加一个第三方依赖,只需要在build.gradle的dependencies方法中添加一条引用的字符串,IDE就能自定帮我们从指定的仓库中下载。这就是我们现在使用的工具了。

2.Gradle的编译流程

1.初始化阶段

gradle支持单项目和多项目的构建,在初始化阶段,gradle决定哪些项目将会参与构建,并为每个项目创建Project对象实例。同时在初始化阶段会更加settting.gradle文件创建一个Setting对象,来配置多项目的Project。

2.配置阶段

在配置阶段,gradle会分别在每个Project上执行build.gradle,并配置Project对象。

 3.执行阶段

在执行阶段,gradle会判断配置阶段创建的哪些Task将要执行,然后执行选中的Task。

响应构建生命周期

在gradle的执行生命周期过程,可以给build.gradle添加一些方法从而添加我们自己的处理流程,如:afterEvaluate()方法来给一个添加一个closure,这个方法添加一个listener监听一个项目的build的过程,在build之后就会执行。

afterEvaluate {
    def assemble = project.tasks.findByName("assemble")
    println("task-name:"+assemble+"---1enable:${assemble.enabled}")
    assemble.setEnabled(false)
    println("task-name:"+assemble+"---2enable:${assemble.enabled}")
    println("size:"+set.size())
    println("ext--name==${ext.name}")
}

Build Lifecycle 构建流程,生命周期

3.Gradle中一些基本的类

Gradle类

        Gradle是基础类,通过project对象的getGradle可以获取到对象,它提供了几个访问项目的方法,以及一些添加回调的方法。比如:allprojects(action)、afterProject。

Project类

Project是使用Gradle最主要的类之一,我们每个build.gradle文件在编译的过程都会被组装成一个project对象实例。提供了基础的访问项目的方法,我们常用的

buildscript、dependencies都是它的方法。

Task类

Task代表了gradle系统中的一个原子操作,比如编译class文件或者生成一个doc文件。没一个Task都属于一个project,我们可以使用一些简单的方法创建task对象。

task myTask
task myTask { configure closure }
task myTask(type: SomeType)
task myTask(type: SomeType) { configure closure }

其中task关键字,实际上时一个方法

Gradle系统提供的一些基础的Task操作:assemble、delete、clean、build

        https://docs.gradle.org/current/userguide/base_plugin.html#sec:base_tasks

Task之间是可以相互依赖的,某个task的执行可能要依赖于某个task的结果。

        截取官网gradle介绍的两个不同的gradle任务,他们的Task依赖关系图:

 Task Action

每个Task由一些列的有序的Action组成,当一个task执行的时候会调用action的execute执行。我们在build.gradle文件中一般可以通过

Task.doFirst(org.gradle.api.Action) or Task.doLast(org.gradle.api.Action)添加一个Action来执行我们的代码逻辑。

Setting类

Setting官方定义:声明实例化和配置参与构建的项目实例的层次结构所需的配置。我们通常使用Setting的两个功能:

1.组织多moudle项目的项目结构,让gradle编译时区别是单项目构建还是多项目构建。

include ':flutter_xxx'

2.设置一些属性

除了此接口的属性之外,settting对象还为设置脚本提供了一些额外的只读属性。包括:buildCacheextensionspluginsrootProject等。

3.设置Gradle

Gradle提供了一些通过属性和方法设置编译环境的方式。这些设置能够改变编译的参数,根据我们的需求优化编译效果,提高编译速度等。

Gradle属性

我们在版本控制中保存一些设置比如 JVM 内存配置和 Java 主目录位置,以便整个团队在一致的环境中工作。我们只需要在gradle.properties文件中添加一些属性即可。

gradle.properties中的一些属性设置,有一些是Android组件自定义的。
 
# 这里是 robolectric 单元测试读取资源用的
android.enableUnitTestBinaryResources=true

base.dir=../Base/base
structure.dir=../Structure/structure
account.dir=../Account/account
common.dir=../Common/common
ad.dir=../Ad/ad

 Gradle提供的其他一些设置

org.gradle.caching=(true,false)

当设置为 true 时,Gradle 将在可能的情况下重用任何先前构建的任务输出,从而使构建速度更快

org.gradle.caching.debug=(true,false)

设置为 true 时,每个任务的单个输入属性哈希值和构建缓存键都会记录在控制台上

org.gradle.daemon=(true,false)

当设置为 true 时,Gradle 守护程序用于运行构建。默认为真

org.gradle.java.home=(path to JDK home)

为 Gradle 构建过程指定 Java 主目录。该值可以设置为 jdk 或 jre 位置,但是,根据您的构建功能,使用 JDK 更安全

org.gradle.jvmargs=(JVM arguments)

指定用于 Gradle 守护程序的 JVM 参数。该设置对于为构建性能配置 JVM 内存设置特别有用,这不会影响 Gradle 客户端 VM 的 JVM 设置

其他设置

设置系统属性、环境变量、以及CI触发执行等,还可以设置通过http代理访问等。

官网设置

Build Environmenticon-default.png?t=LA46https://docs.gradle.org/current/userguide/build_environment.html

4.Gradle插件

apply plugin:java 表示应用了gradle的java插件

apply plugin :MyClass 表示应用指定的class实现的插件。

插件的实现

1.gradle文件插件的实现

通过定义一个如config.gradle脚本文件的方式添加一个脚本插件。这个方式的插件简单方便,但是,这种插件在构建脚本之外不可见,我们不能在定义他的项目外重用。

2.buildSrc方式插件的实现

添加buildSrc moudle的方式,可以让一个项目的多个module使用。

可以将插件的源代码放在 rootProjectDir/buildSrc/src/main/java 目录(或 rootProjectDir/buildSrc/src/main/groovy 或 rootProjectDir/buildSrc/src/main/kotlin)具体取决于我们用哪种语言开发,同时我们在build.gradle中指定资源的位置。Gradle 自动编译和测试插件,这种插件在我们的项目子项目可以使用。

3.独立可发布的插件

创建一个插件,写几行简单的代码,然后使用发布命令发布到本地的maven仓库,通过classpath的方式引入插件,然后通过apply plugin:插件名称 方式使用插件。

通过classpath引入发布的可重用插件

通过将插件添加到构建脚本类路径然后应用插件,可以将已发布为外部 jar 文件的二进制插件添加到项目中。可以使用 buildscript {} 块将外部 jar 添加到构建脚本类路径中,如构建脚本的外部依赖项中所述。

官方示例:加载在指定的插件存储库gradlePluginPortal这个maven地址中的插件。

 代码示例

public class DiguaPlugin :Plugin<Project> {
 
    override fun apply(target: Project) {
        target.tasks.forEach {
            println("DiguaPlugin--task--${it.name}")
        }
        target.task("androidReaderTask"){
            it.doLast {
                println("android reader apply doLast Task")
            }
        }
        target.afterEvaluate {
            println("AndroidReader--自定义gradle插件 end")
        }
    }
 
}

定义一个插件类,实现了Plugin接口,这个接口只有一个apply方法。

发布插件设置

//引入发布的依赖
dependencies {
    implementation gradleApi()
    implementation localGroovy()
}
 
//设置发布组件的Task
uploadArchives {
    repositories {
        mavenDeployer {
            //本地的Maven地址设置为
            repository(url: uri('../repo'))
            pom.groupId = 'com.digua.android.plugin'
            pom.artifactId = 'diguaPlugin'
            pom.version = '1.0.6'
        }
    }
}

在main目录下创建resource目录:

 注意:这里的properties文件的名称就是最后我们apply或者plugins访问插件的名称

 

 最后根据需求编写完成插件后执行对应的uploadArchives的Task就可以发布到对饮的仓库中。

使用组件

 设置maven仓库,然后在classpath中添加gradle插件的路径。调用apply plugin:"pluginName"或者plugins中添加properties文件的名称这样就能访问到插件了。

5.探讨

1.没有Gradle如何编译项目?

2.Gradle还有哪些不常见的特性?

3.Gradle在项目编译中有什么可以改进的地方?

比如可以修改gradle.properties中的设置,根据需要调整编译配置:

比如修改org.gradle.jvmargs = -Xmx1536M,这个可以修改gradle编译VM的内存,影响到编译的速度。因为保活进程会保存每次编译的一些信息,内存大能够保存更多的编译信息,下次编译的时候不需要重新加载文件,直接提高了编译速度。同时也会影响增量编译等。

4.如何能通过修改优化gradle脚本,优化编译速度?

比如我们可以获取所有的Task(./gradlew tasks),然后通过编写条件,过滤掉一些编译不太重要的task,从而加快编译速度。

拓展

Gradle保活进程如何提高编译速度?

The Gradle Daemon

 参考文献

深入理解Android之Gradle_Innost的专栏-CSDN博客_android gradle

Gradle开发快速入门——DSL语法原理与常用API介绍 - Paincker | Paincker

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值