作为Android开发你必须明白的Gradle基础

11,832 阅读15分钟

作为一个Android开发程序员,如果你的build.gradle都只能靠IDE生成或者从别的项目中复制粘贴来完成,那么你该好好的看完这篇文章,掌握一下你不知道的Gradle基础。

文中的图片均来自于网络,侵删

Gradle是一个基于JVM的构建工具,目前Android Studio中建立的工程都是基于gradle进行构建的。Gradle的与其他构建工具(ant、maven)的特性主要包括:

  • 强大的DSL和丰富的gradle的API
  • gradle就是groovy
  • 强大的依赖管理
  • 可拓展性
  • 与其他构建工具的集成

三种构建脚本

Gradle的脚本都是配置型脚本。每一种脚本类型实际上都是某个具体的gradle的API中的类对象的委托,脚本执行对应的其实是其委托的对象的配置。在一个完整的gradle的构建体系中,总共有三种类型的构建脚本,同时也分别对应着三种委托对象

脚本类型 委托对象
Init script Gradle
Settings script Settings
Build script Project

init.gradle

对应的就是上面的Init script,实际上就是Gradle对象的委托,所以在这个init 脚本中调用的任何属性引用以及方法,都会委托给这个 Gradle 实例。

Init script的执行发生在构建开始之前,也是整个构建最早的一步。

配置Init scrip的依赖

每个脚本的执行都可以配置当前脚本本身执行所需要的依赖项。Init scrip的配置如下:

// initscript配置块包含的内容就是指当前脚本本身的执行所需要的配置
// 我们可以在其中配置比如依赖路径等等
initscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath group: 'org.apache.commons', name: 'commons-math', version: '2.0'
    }
}

使用Init scrip

要使用一个定义好的Init scrip,主要有以下几个方式

  • 在执行gradle命令的时候,通过-I--init-script命令选项指定脚本的路径

    这种方式可以针对具体的一次构建。

  • 把一个init.gradle文件放到 *USER_HOME*/.gradle/ 目录

  • 把一个文件名以.gradle结尾的文件放到Gradle 分发包*GRADLE_HOME*/init.d/ 目录内

    以上的两种方式是全局的,对机器内的构建都会起作用

settings.gradle

对应的是Settings script脚本类型,是Settings对象的委托。在 脚本中调用的任何属性引用以及方法,都会委托给这个 Settings 实例。

Settings script的执行发生在gradle的构建生命周期中的初始化阶段。Settings脚本文件中声明了构建所需要的配置,并用以实例化项目的层次结构。在执行settings脚本并初始化Settings对象实例的时候,会自动的构建一个根项目对象rootProject并参与到整个构建当中。(rootProject默认的名称就是其文件夹的名称,其路径就是包含setting脚本文件的路径)。

下面是一张关于Settings对象的类图:

每一个通过include方法被添加进构建过程的project对象,都会在settings脚本中创造一个ProjectDescriptor的对象实例。

因此,在settings的脚本文件中,我们可以访问使用的对象包括:

  • Settings对象
  • Gradle对象
  • ProjectDescriptor对象

获取settings文件

在gradle中,只要根项目/任何子项目的目录中包含有构件文件,那么就可以在相应的位置运行构建。而判断一个构建是否是多项目的构建,则是通过寻找settings脚本文件,因为它指示了子项目是否包含在多项目的构建中。

查找settings文件的步骤如下:

  1. 在与当前目录同层次的master目录中搜索setting文件
  2. 如果在1中没有找到settings文件,则从当前目录开始在父目录中查找settings文件。

当找到settings文件并且文件定义中包含了当前目录,则当前目录就会被认为是多项目的构建中的一部分。

build.gradle

对应的就是前面提到的Build script脚本类型,是gradle中Project对象的委托。在脚本中调用的任何属性引用以及方法,都会委托给这个 Project 实例。

配置脚本依赖

在build.gradle文件中有一个配置块buildScipt{}是用于配置当前脚本执行所需的路径配置等的(与initScript形似)。

buildscript {
	// 这里的repositories配置块要与Project实例当中的repositories区分开来
	// 这里的repositories配置是指脚本本身依赖的仓库源,其委托的对象实际上是ScriptHandler
    repositories {
        mavenLocal()
        google()
        jcenter()
    }
    // 与前面的repositories配置块相同,也要与Project当中的dependencies配置块区分开来
    dependencies {
        classpath 'com.android.tools.build:gradle:3.1.2'
    }
}

这里补充关键的一点,在build.gradle文件中,不管buildScript{}配置块被放在哪个位置,它总是整个脚本文件中最先被执行的

三个构建块

每个gradle构建都包含三个基本的构件块:

  • project
  • task
  • property

每个构建包含至少一个project,进而又包含一个或者多个task。project和task暴露的属性(property)可以用来控制构建。

Project

我们对project的理解更多来源于项目目录中的build.gradle文件(因为它其实就是project对象的委托)。Project对象的类图如下所示:

项目配置

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

常见的例子比如:

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

Task

任务是gradle构建的基础配置块之一,gradle的构建的执行就是task的执行。下面是task的类图。

task的配置和动作

当我们定一个一个task的时候,会包含配置和动作两部分的内容。比如下面的代码示例:

task test{
    println("这是配置")
    
    doFirst{
        // do something here
    }
    doLast(){
        // do something here
    }
}

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

  • doFirst
  • doLast

这些动作是在gradle的构建生命周期中的执行阶段被调用。值得注意的是,一个task可以声明多个doFirstdoLast动作。也可以为一些已有的插件中定义的task添加动作。比如:

// 为test任务添加一个doLast的动作
test.doLast{
    // do something here
}

在task的定义之中,除了动作块以外的是配置块,我们可以声明变量、访问属性、调用方法等等。这些配置块的内容发生在gradle的构建生命周期中的配置阶段。因此task中的配置每次都会被执行。(动作块只有在实际发生task的调用的时候才会执行)。

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的类型

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

task createDistribution(type:Zip){
    
}

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

Property

属性是贯穿在gradle构建始终的,用于帮助控制构建逻辑的存在。gradle中声明属性主要有以下两种方式:

  • 使用ext命名空间定义拓展属性
  • 使用gradle属性文件gradle.properties定义属性

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)
        }
    }
}

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

构建生命周期

前面提及到了gradle中多种脚本类型,并且他们都在不同的生命周期中被执行。

三个阶段

在gradle构建中,构建的生命周期主要包括以下三个阶段:

  • 初始化(Initialization)

    如前文所述,在这个阶段,settings脚本会被执行,从而Gradle会确认哪些项目会参与构建。然后为每一个项目创建 Project 对象。

  • 配置(Configuration)

    配置 Initialization 阶段创建的Project 对象,所有的配置脚本都会被执行。(包括Project中定义的task的配置块也都会被执行)

  • 执行(Configuration)

    这个阶段Gradle会确认哪些在 Configuration 阶段创建和配置的 Task 会被执行,哪些 Task会被执行取决于gradle命令的参数以及当前的目录,确认之后便会执行

监听生命周期

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

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

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

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

Gradle

  • beforeProject()/afterProject()

    等同于Project中的beforeEvaluateafterEvaluate

  • settingsEvaluated()

    settings脚本被执行完毕,Settings对象配置完毕

  • projectsLoaded()

    所有参与构建的项目都从settings中创建完毕

  • projectsEvaluated()

    所有参与构建的项目都已经被评估完

TaskExecutionGraph

  • whenReady()

    task图生成。所有需要被执行的task已经task之间的依赖关系都已经确立

Project

  • beforeEvaluate()
  • afterEvaluate()

依赖管理

在前面提及的Gradle的主要特性之中,其中的一点就是强大的依赖管理。Gradle中具备丰富的依赖类型,兼容多种依赖仓库。同时Gradle中的每一项依赖都是基于特定的范围(scope)进行分组管理的。

在gradle中添加为项目添加依赖的方式如下所示:

// build.gradle

// 添加依赖仓库源
repositories {
    google()
    mavenCentral()
}
// 添加依赖
// 依赖类型包括:文件依赖、项目依赖、模块依赖
dependencies {
    // local dependencies.
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    ...
}

四种依赖类型

Gradle中的依赖类型有四类:

  • 模块依赖

    这是gradle中比较常见的依赖类型, 它通常指向仓库中的一个构件,如下所示:

    dependencies {
        runtime group: 'org.springframework', name: 'spring-core', version: '2.5'
        runtime 'org.springframework:spring-core:2.5',
                'org.springframework:spring-aop:2.5'
        runtime(
            [group: 'org.springframework', name: 'spring-core', version: '2.5'],
            [group: 'org.springframework', name: 'spring-aop', version: '2.5']
        )
        runtime('org.hibernate:hibernate:3.0.5') {
            transitive = true
        }
        runtime group: 'org.hibernate', name: 'hibernate', version: '3.0.5', transitive: true
        runtime(group: 'org.hibernate', name: 'hibernate', version: '3.0.5') {
            transitive = true
        }
    }
    

    模块依赖对应于gradle的API中的 ExternalModuleDependency对象

  • 文件依赖

    dependencies {
        runtime files('libs/a.jar', 'libs/b.jar')
        runtime fileTree(dir: 'libs', include: '*.jar')
    }
    
  • 项目依赖

    dependencies {
        compile project(':shared')
    }
    

    项目依赖对应于gradle的API中的 ProjectDependency对象

  • 特定的Gradle发行版依赖

    dependencies {
        compile gradleApi()
        testCompile gradleTestKit()
        compile localGroovy()
    }
    

管理依赖配置

gradle中项目的每一项依赖都是应用于一个特定的范围的,在gradle中用 Configuration对象表示。每一个Configuration对象都会有一个唯一的名称。Gradle的依赖配置管理如下所示:

自定义Configuration

在gradle中,自定义Configuration对象是非常简单的,同时定义自己的Configuration对象的时候,也可以继承于已有的Configuration对象,如下所示:

configurations {
    jasper
    // 定义继承关系
    smokeTest.extendsFrom testImplementation
}

repositories {
    mavenCentral()
}

dependencies {
    jasper 'org.apache.tomcat.embed:tomcat-embed-jasper:9.0.2'
}

管理传递性依赖

在实际的项目依赖管理中存在这样的一种依赖关系:

  • 模块b依赖于模块c
  • 模块a依赖于模块b
  • 模块c成为了模块a的传递依赖

在处理上面这种传递性依赖的时候,gradle提供了强大的管理功能

使用依赖约束

依赖约束可以帮助我们控制传递性依赖以及自身的依赖的版本号(版本范围),比如:

dependencies {
    implementation 'org.apache.httpcomponents:httpclient'
    constraints {
        // 这里httpclient是项目本身的依赖
        // 这个约束表示,不管是项目本身的依赖是还是传递依赖都强制使用这个指定的版本号
        implementation('org.apache.httpcomponents:httpclient:4.5.3') {
            because 'previous versions have a bug impacting this application'
        }
        // commons-codec并没有被声明为项目本身的依赖
        // 所以仅当commons-codec是传递性依赖的时候这段逻辑才会被触发
        implementation('commons-codec:commons-codec:1.11') {
            because 'version 1.9 pulled from httpclient has bugs affecting this application'
        }
    }
}

排除特定的传递性依赖

有的时候,我们所依赖的项目/模块会引入多个传递性依赖。而其中部分的传递性依赖我们是不需要的,这时候可以使用exclude排除部分的传递性依赖,如下所示:

dependencies {
    implementation('log4j:log4j:1.2.15') {
        exclude group: 'javax.jms', module: 'jms'
        exclude group: 'com.sun.jdmk', module: 'jmxtools'
        exclude group: 'com.sun.jmx', module: 'jmxri'
    }
}

强制使用指定的依赖版本

Gradle通过选择依赖关系图中找到的最新版本来解决任何依赖版本冲突。 可是有的时候,某些项目会需要使用一个较老的版本号作为依赖。这时候我们可以强制指定某一个版本。例如:

dependencies {
    implementation 'org.apache.httpcomponents:httpclient:4.5.4'
    // 假设commons-codec的最新版本是1.10
    implementation('commons-codec:commons-codec:1.9') {
        force = true
    }
}

要注意的是,如果依赖项目中使用了新版本才有的api,而我们强制使用了旧版本的传递依赖之后,会引起运行时的错误

禁止传递性依赖

dependencies {
    implementation('com.google.guava:guava:23.0') {
        transitive = false
    }
}

依赖关系解析

使用依赖关系解析规则

依赖关系解析规则提供了一种非常强大的方法来控制依赖关系解析过程,并可用于实现依赖管理中的各种高级模式。比如:

  • 统一构件组的版本

    很多时候我们依赖一个公司的库会包含多个module,这些module一般都是统一构建、打包和发布的,具备相同的版本号。这个时候我们可以通过控制依赖关系的解析过程做到版本号统一。

    configurations.all {
        resolutionStrategy.eachDependency { DependencyResolveDetails details ->
            if (details.requested.group == 'org.gradle') {
                details.useVersion '1.4'
                details.because 'API breakage in higher versions'
            }
        }
    }
    
  • 处理自定义的版本scheme

    configurations.all {
        resolutionStrategy.eachDependency { DependencyResolveDetails details ->
            if (details.requested.version == 'default') {
                def version = findDefaultVersionInCatalog(details.requested.group, details.requested.name)
                details.useVersion version.version
                details.because version.because
            }
        }
    }
    
    def findDefaultVersionInCatalog(String group, String name) {
        //some custom logic that resolves the default version into a specific version
        [version: "1.0", because: 'tested by QA']
    }
    

关于更多依赖关系解析规则的使用实例可以参考gradle的API中的 ResolutionStrategy

使用依赖关系的替代规则

依赖关系的替换规则和上面的依赖关系解析规则有点相似。实际上,依赖关系解析规则的许多功能可以通过依赖关系替换规则来实现。依赖关系的替换规则允许项目依赖(Project Dependency)和模块依赖(Module Dependency)被指定的替换规则透明地替换。

// 使用项目依赖替换模块依赖
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute module("org.utils:api") with project(":api") because "we work with the unreleased development version"
        substitute module("org.utils:util:2.5") with project(":util")
    }
}
// 使用模块依赖替换项目依赖
configurations.all {
    resolutionStrategy.dependencySubstitution {
        substitute project(":api") with module("org.utils:api:1.3") because "we use a stable version of utils"
    }
}

除了上面两种之外,还有其他三种的依赖关系规则处理。因为没有实际使用过,这里不过多阐述,想了解更多可以查看官方的文档Customizing Dependency Resolution Behavior

  • 使用组件元数据(meta-data)规则
  • 使用组件选择规则
  • 使用模块更换规则

插件开发

插件开发是gradle灵活的构建体系中的一个强大工具。通过gradle中的PluginAPI,我们可以自定义插件,把一些通用的构建逻辑插件化并广泛的运用。比如Android项目中都会使用的:com.android.application,kotlin-android,java等等。

网上关于插件开发的文章已经很多,这里不再赘述。这里推荐我写的一个Gradle插件,也是在我完全看了gradle的官方文档之后,结合前面提及到的依赖管理的知识写的:

  • EasyDependency

    一个帮助提高组件化开发效率的gradle插件,提供的功能包括:

    1. 发布模块的构件都远程maven仓库
    2. 动态更换依赖配置:对模块使用源码依赖或者maven仓库的构件(aar/jar)依赖

写在最后

全文基本是在看了gradle的官方文档及相关资料之后,按照自己的思路的整理和总结。关于gradle的使用和问题欢迎一起讨论。