欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

关于如何使用 gradle 的教程,一篇文章就够了

最编程 2024-06-29 19:16:40
...

概述

Gradle是新一代构建工具,从0.x版本一路走来虽然国内可寻的资料多了一些,但都是比较碎片化的知识。官方的Userguide虽然是业内良心之作,但无奈太长,且版本变化较快,又鉴于很多同学一看到英文内心便已认定无法读懂,遂打算利用业余时间攒此本《跟我学gradle》,希望通过此书可以降低学习曲线能让希望使用Gradle的同学更轻易地入门。

简介

Gradle是继Maven之后的新一代构建工具,它采用基于groovy的DSL语言作为脚本,相比传统构建工具通过XML来配置而言,最直观上的感受就是脚本更加的简洁、优雅。如果你之前对Maven有所了解,那么可以很轻易的转换到Gradle,它采用了同Maven一致的目录结构,可以与Maven一样使用Maven*仓库以及各类仓库的资源,并且Gradle默认也内置了脚本转换命令可以方便的将POM转换为gradle.build。

Gradle的优势

依赖管理:即将你项目中的jar包管理起来,你可以使用Maven或者Ivy的远程仓库、或者本地文件系统等
编译打包:可以通过脚本实现花样打包,包括修改文件、添加抑或排除某些类或资源、采用指定JDK版本构建、打包后自动上传等等等等
多项目支持: Gradle对多项目有着良好的支持,比如一个很具有代表性的实践就是spring framework
多语言支持:无论是java、groovy、scala、c++都有良好的支持
跨平台支持:gradle是基于jvm的,只要有jvm你就可以让gradle运行
灵活的的脚本:你可以使用groovy灵活的编写任务完成你想要做的任何事情

约定优于配置

约定优于配置(convention over configuration),简单而言就是遵循一定的固定规则从而可以避免额外的配置。虽然这一定程度上降低了灵活性,但却能减少重复的额外配置,同时也可以帮助开发人员遵守一定的规则。当然,约定并不是强制性约束,Gradle提供了各种灵活的途径可以让你更改默认的配置。

标准结构

Gradle遵循COC(convention over configuration约定优于配置)的理念,默认情况下提供了与maven相同的项目结构配置
大体结构如下

project root
src/main/java(测试)
src/main/resources
src/test/java(测试源码目录)
src/test/resources(测试资源目录)
src/main/webapp(web工程)

非标准结构配置

在一些老项目上,可能目录结构并不是标准结构,然而一般开发人员又不好进行结构调整.此时可以通过配置sourceSet来指定目录结构

sourceSets {
    main {
        java {
            srcDir 'src/java'
        }
        resources {
            srcDir 'src/resources'
        }
    }
}

或者采用如下写法也是可以的

sourceSets {
    main.java.srcDirs = ['src/java']
    main.resources.srcDirs = ['src/resources']
}

在android中

android {
    sourceSets {
        main {
            manifest.srcFile 'AndroidManifest.xml'
            java.srcDirs = ['src']
            resources.srcDirs = ['src']
            aidl.srcDirs = ['src']
            renderscript.srcDirs = ['src']
            res.srcDirs = ['res']
            assets.srcDirs = ['assets']
        }

        androidTest.setRoot('tests')
    }
}

当然如果你的资源目录与源码目录相同这样就比较....了,但你仍然可以按照如下方式搭配include和exclude进行指定

sourceSets {
  main {
    java {
      //your java source paths and exclusions go here...
    }

    resources {
      srcDir 'main/resources'
      include '**/*.properties'
      include '**/*.png'


      srcDir 'src'
      include '**/Messages*.properties'
      exclude '**/*.java'
    }
  }
}

一个简单的Gralde脚本,或许包含如下内容,其中标明可选的都是可以删掉的部分

  • 插件引入:声明你所需的插件
  • 属性定义(可选):定义扩展属性
  • 局部变量(可选):定义局部变量
  • 属性修改(可选):指定project自带属性
  • 仓库定义:指明要从哪个仓库下载jar包
  • 依赖声明:声明项目中需要哪些依赖
  • 自定义任务(可选):自定义一些任务
//定义扩展属性(给脚本用的脚本)
buildScript {
    repositories {
         mavenCentral()
    }
}
//应用插件,这里引入了Gradle的Java插件,此插件提供了Java构建和测试所需的一切。
apply plugin: 'java'
//定义扩展属性(可选)
ext {
    foo="foo"
}
//定义局部变量(可选)
def bar="bar"

//修改项目属性(可选)
group 'pkaq'
version '1.0-SNAPSHOT'

//定义仓库,当然gradle也可以使用各maven库 ivy库 私服 本地文件等,后续章节会详细介绍(可选)
repositories {
    jcenter()
}

//定义依赖,这里采用了g:a:v简写方式,加号代表了最新版本(可选)
dependencies {
    compile "cn.pkaq:ptj.tiger:+"
}

//自定义任务(可选)
task printFoobar {
    println "${foo}__${bar}"
}

buildscript 配置该 Project 的构建脚本的 classpath,在 Andorid Studio 中的 root project 中可以看到
apply(options: Map<String, ?>)我们通过该方法使用插件或者是其他脚本,options里主要选项有:

from: 使用其他脚本,值可以为 Project.uri(Object) 支持的路径
plugin:使用其他插件,值可以为插件id或者是插件的具体实现类

apply plugin: 'com.android.application'
//使用插件,MyPluginImpl 就是一个Plugin接口的实现类
apply plugin: MyPluginImpl

//引用其他gradle脚本,push.gradle就是另外一个gradle脚本文件
apply from: './push.gradle'

扩展:
Gradle脚本是基于groovy的DSL,这里对上述脚本与Gradle api的关系稍作解释,希望可以帮助你更好的理解groovy与gradle dsl之间的关系

//project 的buildScript方法  实际返回的是一个ScriptHandler对象
buildScript {
    repositories {
         mavenCentral()
    }
}
//调用apply方法 参数是一个 map,调用时参数省略了括号
apply plugin: 'java'
//定义扩展属性(可选)
ext {
    foo="foo"
}
//定义局部变量(可选)
def bar="bar"

//修改项目属性(可选)
group 'pkaq'
version '1.0-SNAPSHOT'

//project 的repositories 方法  实际返回的是一个RepositoryHandler对象
repositories {
    jcenter()
}

//project 的dependencies 方法  实际返回的是一个DependencyHandler对象
dependencies {
    compile "cn.pkaq:ptj.tiger:+"
}

//调用Task task(String name, Closure configureClosure)方法
task printFoobar {
    println "${foo}__${bar}"
}

  • Project对象 : https://docs.gradle.org/current/dsl/org.gradle.api.Project.html

什么是依赖管理

通常而言,依赖管理包括两部分,对依赖的管理以及发布物的管理;依赖是指构建项目所需的构件(jar包等)。例如,对于一个应用了spring普通的java web项目而言,spring相关jar包即项目所需的依赖。发布物,则是指项目产出的需要上传的项目产物。

采用变量统一控制版本号

dependencies {
    def bootVersion = "1.3.5.RELEASE"
    compile     "org.springframework.boot:spring-boot-starter-web:${bootVersion}",  
                "org.springframework.boot:spring-boot-starter-data-jpa:${bootVersion}",
                "org.springframework.boot:spring-boot-starter-tomcat:${bootVersion}"
}

自动获取最新版本依赖

如果你想某个库每次构建时都检查是否有新版本,那么可以采用+来让Gradle在每次构建时都检查并应用最新版本的依赖。当然也可以采用1.x,2.x的方式来获取某个大版本下的最新版本。

dependencies {
    compile     "org.springframework.boot:spring-boot-starter-web:+"
}

依赖的坐标

仓库中构件(jar包)的坐标是由configurationName "group:name:version:classifier@extension"组成的字符串构成,如同Maven中的GAV坐标,Gradle可借由此来定位你想搜寻的jar包。

在gradle中可以通过以下方式来声明依赖:

testCompile group: 'junit', name: 'junit', version: '4.0'

在gradle中可以通过以下方式来声明依赖:

testCompile group: 'junit', name: 'junit', version: '4.0'
项目 描述
configurationName 依赖的作用范围,具体介绍看本章第二小节
group 通常用来描述组织、公司、团队或者其它有象征代表意义的名字,比如阿里就是com.alibaba,一个group下一般会有多个artifact
name 依赖的名称,或者更直接来讲叫包名、模块、构件名、发布物名以及随便你怎么称呼。druid就是com.alibaba下的一个连接池库的名称
version 见名知意,无它,版本号。
classifier 类库版本,在前三项相同的情况下,如果目标依赖还存在对应不同JDK版本的版本,可以通过此属性指明
extension 依赖的归档类型,如aar、jar等,默认不指定的话是jar

这是由于Gradle依赖配置支持多种书写方式,采用map或者字符串。

// 采用map描述依赖
testCompile group: 'junit', name: 'junit', version: '4.0'
// 采用字符串方式描述依赖
testCompile 'junit:junit:4.0'

显然采用字符串的方式更加简单直观,当然借助groovy语言强大的GString还可以对版本号进行抽离。如下面的示例,这里需要注意的是如果要用GString的话,依赖描述的字符串要用""双引号包起来才会生效。

def ver = "4.0"
testCompile "junit:junit:${ver}"

依赖的范围

上面的例子中采用的testComplie是声明依赖的作用范围,关于各种作用范围的功效可见下表。

tip:这里需要注意的是,provided范围内的传递依赖也不会被打包

名称 说明
compileOnly gradle2.12之后版本新添加的,2.12版本时期曾短暂的叫provided,后续版本已经改成了compileOnly,由java插件提供,适用于编译期需要而不需要打包的情况
providedCompile war插件提供的范围类型:与compile作用类似,但不会被添加到最终的war包中这是由于编译、测试阶段代码需要依赖此类jar包,而运行阶段容器已经提供了相应的支持,所以无需将这些文件打入到war包中了;例如Servlet API就是一个很明显的例子.
api 3.4以后由java-library提供 当其他模块依赖于此模块时,此模块使用api声明的依赖包是可以被其他模块使用
implementation 3.4以后由java-library提供 当其他模块依赖此模块时,此模块使用implementation声明的依赖包只限于模块内部使用,不允许其他模块使用。
compile 编译范围依赖在所有的classpath中可用,同时它们也会被打包。
providedRuntime 同proiveCompile类似。
runtime runtime依赖在运行和测试系统的时候需要,但在编译的时候不需要。比如,你可能在编译的时候只需要JDBC API JAR,而只有在运行的时候才需要JDBC驱动实现。
testCompile 测试期编译需要的附加依赖
testRuntime 测试运行期需要
archives -
default 配置默认依赖范围

依赖的分类

类型 描述
外部依赖 依赖存放于外部仓库中,如jcenter ,mavenCentral等仓库提供的依赖
项目依赖 依赖于其它项目(模块)的依赖
文件依赖 依赖存放在本地文件系统中,基于本地文件系统获取依赖
内置依赖 跟随Gradle发行包或者基于Gradle API的一些依赖,通常在插件开发时使用
子模块依赖 还没搞清楚是什么鬼

外部依赖

可以通过如下方式声明外部依赖,Gradle支持通过map方式或者g:a:v的简写方式传入依赖描述,这些声明依赖会去配置的repository查找。

dependencies {
 // 采用map方式传入单个
  compile group: 'commons-lang', name: 'commons-lang', version: '2.6'
 // 采用map方式传入多个
  compile(
      [group: 'org.springframework', name: 'spring-core', version: '2.5'],
      [group: 'org.springframework', name: 'spring-aop', version: '2.5']
  )
  // 采用简写方式声明
  compile 'org.projectlombok:lombok:1.16.10' 
  // 采用简写方式传入多个 
  compile 'org.springframework:spring-core:2.5',
          'org.springframework:spring-aop:2.5'

}

项目依赖

此类依赖多见于多模块项目,书写方式如下,其中:是基于跟项目的相对路径描述符。

 compile project(':project-foo')

文件依赖

依赖存在于本地文件系统中,举个栗子,如oracle的OJDBC驱动,*仓库中没有又没有自建私服此时需要放到项目lib下进行手工加载那么便可采用此种方式,可以通过FileCollection接口及其子接口提供的方法加载这些依赖(支持文件通配符)

dependencies {
   // 指定多个依赖
   compile files('hibernate.jar', 'libs/spring.jar')

   // 读取lib文件夹下的全部文件作为项目依赖
   compile fileTree('libs')

   // 根据指定基准目录\包含\排除条件加载依赖
   compile fileTree(dir:'libs',include:'spring*.jar',exclude:'hibernate*.jar')
 }

内置依赖

跟随Gradle发行包或者基于Gradle API的一些依赖,通常在插件开发时使用,当前提供了如下三种

 dependencies {
   // 加载Gradle自带的groovy作为依赖
   compile localGroovy()

   // 使用Gradle API作为依赖
   compile gradleApi()

   /使用 Gradle test-kit API 作为依赖
   testCompile gradleTestKit()
 }

子模块依赖

简单来说就是声明依赖的依赖或者依赖的传递依赖,一般情况下如果依赖的库并未用构建工具构建(尤其是一些上古时代的老库),那么Gradle是无法透过源文件去查找该库的传递性依赖的,通常而言,一个模块采用XML(POM文 件)来描述库的元数据和它的传递性依赖。Gradle可以借由此方式提供相同的能力,当然这种方式也会可以改写原有的传递性依赖。这里让druid连接池依赖了ptj.tiger的一个库。

dependencies {
    // 让ptj.tiger作为druid的传递性依赖
    compile module("com.alibaba:druid:1.0.26") {
            dependency("cn.pkaq:ptj.tiger:+")
    }

    runtime module("org.codehaus.groovy:groovy:2.4.7") {
        // 停用groovy依赖的commons-cli库的依赖传递
        dependency("commons-cli:commons-cli:1.0") {
            transitive = false
        }
        // 让groovy依赖的ant模块的依赖ant-launcher停用传递性依赖并依赖ant-junit..........
        module(group: 'org.apache.ant', name: 'ant', version: '1.9.6') {
            dependencies "org.apache.ant:ant-launcher:1.9.6@jar",
                         "org.apache.ant:ant-junit:1.9.6"
        }
    }
 }

什么是传递依赖

在Maven仓库中,构件通过POM(一种XML文件)来描述相关信息以及传递性依赖。Gradle 可以通过分析 该文件获取获取所以依赖以及依赖的依赖和依赖的依赖的依赖,为了更加直观的表述,可以通过下面的输出 结果了解。

image

可以看到,我们的项目依赖了com.android.support-v4包,然而com.android.support-v4包却依赖了一众support的全家桶,借助Gradle的传递性依赖特性,你无需再你的脚本中把这些依赖都声明一遍,你只需要简单的一行,Gradle便会帮你将传递性依赖一起下载下来。

传递依赖特性可以轻松地通过transitive参数进行开启或关闭,上面的示例中如果要忽略com.android.support-v4的传递性依赖可以采用指定 transitive = false 的方式来关闭依赖传递特性,也可以采用添加@jar的方式忽略该依赖的所有传递性依赖。

 compile('com.android.support:support-v4:23.1.1'){
        transitive = false
 }

 compile 'com.android.support:support-v4:23.1.1'@jar

当然,你也可以全局性的关闭依赖的传递特性。

 configurations.all {
   transitive = false
}

排除依赖

有些时候你可能需要排除一些传递性依赖中的某个模块,此时便不能靠单纯的关闭依赖传递特性来解决了。这时exclude就该登场了,如果说@jar彻底的解决了传递问题,那么exclude则是部分解决了传递问题。然而实际上exclude肯能还会用的频率更更频繁一些,比如下面几种情况。

可以通过configuration配置或者在依赖声明时添加exclude的方式来排除指定的引用。

exclude可以接收group和module两个参数,这两个参数可以单独使用也可以搭配使用,具体理解如下:

compile('com.github.nanchen2251:CompressHelper:1.0.5'){
        //com.android.support:appcompat-v7:23.1.1
        exclude group: 'com.android.support'//排除组织依赖
        exclude module: 'appcompat-v7'//排除模块依赖
 }

强制使用版本

当然,有时候你可能仅仅是需要强制使用某个统一的依赖版本,而不是排除他们,那么此时force就该登场了。指定force = true属性可以冲突时优先使用该版本进行解决。

compile('com.github.nanchen2251:CompressHelper:1.0.5'){
        force = true
 }

全局配置强制使用某个版本的依赖来解决依赖冲突中出现的依赖

configurations.all {
   resolutionStrategy {
       force 'com.github.nanchen2251:CompressHelper:1.0.5'
   }
}

另一个例子

//解决冲突 同一版本
configurations.all {
    resolutionStrategy.eachDependency { DependencyResolveDetails details ->
        def requested = details.requested
        if (requested.group == 'com.android.support') {
            if (requested.name.startsWith("support-")||
                    requested.name.startsWith("animated")||
                    requested.name.startsWith("cardview")||
                    requested.name.startsWith("design")||
                    requested.name.startsWith("gridlayout")||
                    requested.name.startsWith("recyclerview")||
                    requested.name.startsWith("transition")||
                    requested.name.startsWith("appcompat")) {
                details.useVersion '25.0.0'
            }
        }
    }
}

使用动态版本

如果你想让你的工程始终采用最新依赖,那么Gradle提供了一种方式可以始终保证采用依赖的最新版本而无需每次手工检查修改版本。

使用加号+,可以让Gradle在每次执行构建时检查远程仓库是否存在该依赖的新版本,如果存在新版本则下载选用最新版本。当然也可以指定依赖某个大版本下的最新子版本,1.+表示始终采用该依赖最新的1.x版本的最新依赖。

  compile 'com.android.support:support-v4:+'//下载最新
  compile 'com.android.support:support-v4:23+'//基于23这个版本最新

  • 虽然这是看上去十分风骚的一种用法,但这无疑会降低你系统构建的速度同时提高构建失败的风险。因为Gradle不得不每次检查远程仓库是否存在最新版本,同时新版本也可能带来无法预知的兼容性问题。

一个综合案例

compile('com.github.nanchen2251:CompressHelper:1.0.5') {
   // 冲突时优先使用该版本
   force = true

   // 依据构建名称排除
   exclude module: 'CompressHelper' 
   // 依据组织名称排除
   exclude group: 'com.github.nanchen2251' 
   // 依据组织名称+构件名称排除
   exclude group: 'com.github.nanchen2251', module: 'CompressHelper' 

   // 为本依赖关闭依赖传递特性
   transitive = false
}

使用config.gradle文件统一管理项目依赖

  1. 在项目的根目录下创建config.gradle文件

    image
  2. 编辑 config.gradle,定义项目依赖

在app的build.gradle中,我们通常需要配置两个部分

  • Android 目录下的项目的版本/包名/编译版本等信息
  • dependencies 目录下的安卓Support库和我们自己引用的第三方库
    所以通常我们在config.gradle文件也将依赖分成两个部分 android/dependencies
ext {

    android = [
            compileSdkVersion      : 28,
            buildToolsVersion      : "28.0.0",
            applicationId          : "com.will.weiyue",
            minSdkVersion          : 19,
            targetSdkVersion       : 28,
            versionCode            : 1,
            versionName            : "1.0"
    ]
    //因为support库都是同一个版本,单独拎出来,方便修改
    dependVersion = [
            support: "28.0.0-alpha3"
    ]

    dependencies = [
            // android-support
            "support-v4"            : "com.android.support:support-v4:${dependVersion.support}",
            "appcompat-v7"          : "com.android.support:appcompat-v7:${dependVersion.support}",
            "design"                : "com.android.support:design:${dependVersion.support}",
            "recyclerview"          : "com.android.support:recyclerview-v7:${dependVersion.support}",
            "cardview"              : "com.android.support:cardview-v7:${dependVersion.support}",
    ]
}

  1. 在 项目的 build.gradle文件中引用config.gradle文件
// 在项目build.gradle文件的最外层添加引用
apply from: "config.gradle"

  1. 修改app的build.gradle文件中的项目引用
// 将android 和 dependencies下的引用都指向 config.gradle
// rootProject.ext.android/dependencies  config.gradle文件的路径
android {
    compileSdkVersion rootProject.ext.android.compileSdkVersion
    defaultConfig {
        applicationId rootProject.ext.android.applicationId
        minSdkVersion rootProject.ext.android.minSdkVersion
        targetSdkVersion rootProject.ext.android.targetSdkVersion
        versionCode rootProject.ext.android.versionCode
        versionName rootProject.ext.android.versionName
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation rootProject.ext.dependencies["appcompat-v7"]
    implementation rootProject.ext.dependencies["constraint-layout"]
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

更新依赖

在执行build、compile等任务时会解析项目配置的依赖并按照配置的仓库去搜寻下载这些依赖。默认情况下,Gradle会依照Gradle缓存->你配置的仓库的顺序依次搜寻这些依赖,并且一旦找到就会停止搜索。如果想要忽略本地缓存每次都进行远程检索可以通过在执行命令时添加--refresh-dependencies参数来强制刷新依赖。

gradle build --refresh-dependencies

当远程仓库上传了相同版本依赖时,有时需要为缓存指定一个时效去检查远程仓库的依赖笨版本,Gradle提供了cacheChangingModulesFor(int, java.util.concurrent.TimeUnit) ,cacheDynamicVersionsFor(int, java.util.concurrent.TimeUnit)两个方法来设置缓存的时效

configurations.all {
    //每隔24小时检查远程依赖是否存在更新
    resolutionStrategy.cacheChangingModulesFor 24, 'hours'
    //每隔10分钟..
    //resolutionStrategy.cacheChangingModulesFor 10, 'minutes'
    // 采用动态版本声明的依赖缓存10分钟
    resolutionStrategy.cacheDynamicVersionsFor 10*60, 'seconds'
}

dependencies {
    // 添加changing: true
    compile group: "group", name: "module", version: "1.1-SNAPSHOT", changing: true
    //简写方式
    //compile('group:module:1.1-SNAPSHOT') { changing = true }
}

缓存管理

缓存位置管理

Gradle在按照配置的仓库去搜寻下载依赖时,下载的依赖默认会缓存到USER_HOME/.gradle/caches目录下,当然也可以手工修改这个位置。
具体可以参考如下三种方式:

  • 通过添加系统变量 GRADLE_USER_HOME
  • 设置虚拟机参数 org.gradle.user.home 属性
  • 通过命令行-g或者 --gradle-user-home 参数设置

离线模式(总是采用缓存内容)

Gradle提供了一种离线模式,可以让你构建时总是采用缓存的内容而无需去联网检查,如果你并未采用动态版本特性且可以确保项目中依赖的版本都已经缓存到了本地,这无疑是提高构建速度的一个好选择。开启离线模式只需要在执行命令时候添加--offline参数即可。当然,采用这种模式的也是有代价的,如果缓存中搜寻不到所需依赖会导致构建失败。

gradle build --offline

依赖-构件的上传与发布

借助maven-publish插件可以轻松地将jar包发布到仓库中。这个过程没啥幺蛾子直接上代码吧。了解更多配置可以查看 Maven plugin插件章节

apply plugin: 'maven-publish'
apply plugin: 'java'

// 打包源文件
task sourceJar(type: Jar) {
    from sourceSets.main.allSource
    classifier = 'sources'
}

task javadocJar(type: Jar, dependsOn: javadoc) {
    classifier = 'javadoc'
    from javadoc.destinationDir
}

publishing {
     // 目标仓库
    repositories {
        maven {
           url "xxx"
        }
    }   

    publications {          
        mavenJava(MavenPublication) {
            // 设置gav属性
            groupId 'org.pkaq'
            artifactId 'tiger'
            version '1.1'

            from components.java
            artifact sourceJar

             // 设置pom相关信息
            pom.withXml {
                Node root = asNode()              
                root.appendNode('description', 'bazinga!')
            }
        }
    }

}
//生成一个元的pom文件
model {
    tasks.generatePomFileForMavenJavaPublication {
        destination = file("$buildDir/generated-pom.xml")
    }
}

检查依赖

在引用的依赖或传递性依赖存在版本冲突时,Gradle采用的策略是优先选取最新的依赖版本解决版本冲突问题。解决此类问题我们可以通过上一章节中介绍的各种依赖管理方法进行排除、强制指定一个版本或者干脆禁用依赖传递特性解决。但如何知道哪些依赖传递了哪些子依赖,哪些传递的依赖又被Gradle进行了隐性升级呢。采用下面的命令可以查看各个范围的依赖树。

gradle  dependencies > dep.log

输出结果:

dependencies

------------------------------------------------------------
Root project
------------------------------------------------------------

archives - Configuration for archive artifacts.
No dependencies

compile - Dependencies for source set 'main'.
+--- org.springframework.boot:spring-boot-starter-web:1.4.2.RELEASE
|    +--- org.springframework.boot:spring-boot-starter:1.4.2.RELEASE
|    |    +--- org.springframework.boot:spring-boot:1.4.2.RELEASE
|    |    |    +--- org.springframework:spring-core:4.3.4.RELEASE
|    |    |    |    \--- commons-logging:commons-logging:1.2
|    |    |    \--- org.springframework:spring-context:4.3.4.RELEASE
|    |    |         +--- org.springframework:spring-aop:4.3.4.RELEASE
|    |    |         |    +--- org.springframework:spring-beans:4.3.4.RELEASE
|    |    |         |    |    \--- org.springframework:spring-core:4.3.4.RELEASE (*)
|    |    |         |    \--- org.springframework:spring-core:4.3.4.RELEASE (*)
|    |    |         +--- org.springframework:spring-beans:4.3.4.RELEASE (*)
|    |    |         +--- org.springframework:spring-core:4.3.4.RELEASE (*)
|    |    |         \--- org.springframework:spring-expression:4.3.4.RELEASE
....
....
省略的
....
....
\--- org.apache.tomcat.embed:tomcat-embed-jasper:8.5.4
     +--- org.apache.tomcat.embed:tomcat-embed-core:8.5.4 -> 8.5.6
     +--- org.apache.tomcat.embed:tomcat-embed-el:8.5.4 -> 8.5.6
     \--- org.eclipse.jdt.core.compiler:ecj:4.5.1

后面dep.log文件名可以随意,然而,你一定在想为什么有些带了(*)有的带了->有的什么都没有呢,这是什么鬼。前面已经说过,当发生版本冲突时Gradle会采用最新版本解决。仔细观察带了(*)的依赖你会发现这些依赖被不同的库重复依赖了若干次,这里(*)的意思即是表示该依赖被忽略掉了。而->则表示其它的定级依赖的传递依赖中存在更高版本的依赖,该版本将会使用->后面的版本来替代。

反向查找

如果你想知道某个依赖到底被哪个库引用过,可以采用下面的命令进行反向查找

gradle dependencyInsight  --dependency tomcat-embed-core > reverse.log 

:dependencyInsight
org.apache.tomcat.embed:tomcat-embed-core:8.5.6 (conflict resolution)
+--- org.apache.tomcat.embed:tomcat-embed-websocket:8.5.6
|    \--- org.springframework.boot:spring-boot-starter-tomcat:1.4.2.RELEASE
|         \--- org.springframework.boot:spring-boot-starter-web:1.4.2.RELEASE
|              \--- compile
\--- org.springframework.boot:spring-boot-starter-tomcat:1.4.2.RELEASE (*)

org.apache.tomcat.embed:tomcat-embed-core:8.5.4 -> 8.5.6
\--- org.apache.tomcat.embed:tomcat-embed-jasper:8.5.4
     \--- compile

(*) - dependencies omitted (listed previously)

BUILD SUCCESSFUL

Total time: 6.936 secs

上面的报告中可以看到8.5.6这个版本后面标注了(conflict resolution) 说明了该版本是用于解决冲突选用的版本。

冲突即停

Gradle默认采用自动升级版本的方式解决依赖冲突,有时这种隐式升级可能会带来一些不必要的麻烦,此时我们可以通过更改这种默认行为来让Gradle发现版本冲突时立即停止构建并抛出错误信息。
更改脚本:

configurations.all {
  resolutionStrategy {
    failOnVersionConflict()
  }
}

执行gradle build的输出结果:

:compileJava FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Could not resolve all dependencies for configuration ':compileClasspath'.
> A conflict was found between the following modules:
   - org.apache.tomcat.embed:tomcat-embed-core:8.5.4
   - org.apache.tomcat.embed:tomcat-embed-core:8.5.6

可以看到在执行gradle build时由于tomcat-embed-core存在版本冲突导致了构建失败,并提示了冲突的两个版本。

依赖报告

Gradle官方提供了一个名叫project-report插件可以让依赖查看更加简单方便,要用此插件只需要在脚本中添加apply plugin: 'project-report'即可。该插件提供的任务可以参考下面的表格,所有输出结果将被放在build/report下。

任务名称 描述
dependencyReport 将项目依赖情况输出到txt文件中 功能同gradle dependencies > build/dependenciestxt
htmlDependencyReport 生成HTML版本的依赖情况
propertyReport 生成项目属性报告
taskReport 生成项目任务报告
projectReport 生成项目报告,包括前四个

使用插件检查更新

使用三方插件进行检查,可以使依赖固定在一个相对新的版本,这里需要注意的是,plugins需要放置在脚本的顶部,更多关于plugins的内容可以查看官方文档

plugins {
   id "name.remal.check-dependency-updates" version "1.0.6" 
}

应用此插件后,可以执行gradle checkDependencyUpdatesgradle cDU 检查依赖最新版本 : }

> Task :web:checkDependencyUpdates
New dependency version: com.alibaba:druid: 1.0.29 -> 1.1.7

多项目构建

前面我们介绍的例子,都是单独执行某一个 build.gradle 文件。但是我们在 Android 应用开发中,一个 Project 可以包含若干个 module ,这种就叫做多项目构建。在 Android Studio 项目中,根目录都有一个名叫 settings.gradle 的文件,然后每个 module 的根目录中又有一个 build.gradle 文件,Gradle 就是通过 settings.gradle 来进行多项目构建的。

通过 settings.gradle 引入子项目

1.先创建如下几个目录及文件:

image

如图所示,在项目根目录创建一个 settings.gradle,在根目录、app以及library目录下也都创建一个 build.gradle 文件。

2.在 settings.gradle 里引入子项目

include ":app", ":library"

3.在 build.gradle 里增加测试代码

//在根目录 build.gradle 里增加
println "-----root file config-----"

//在 app/build.gradle 里增加
println "-----app config-----"

//在 library/build.gradle 里增加
println "-----library config-----"

4.在项目根目录执行命令 gradle -q,结果如下:

-----root file config-----
-----app config-----
-----library config-----

这是一个多项目构建的简单例子,可以看到结构与我们的 Android 项目是类似的。Gradle 在运行时会读取并解析 settings.gradle 文件,生成一个 Settings对象,然后从中读取并解析子项目的 build.gradle 文件,然后为每个 build.gradle 文件生成一个 Project 对象,进而组装一个多项目构建出来。

Settings 里最核心的API就是 include 方法,通过该方法引入需要构建的子项目。

include(projectPaths: String...)

这里我们为每个 build.gradle 文件生成了一个 Project 对象,跟总共3个 Project,根目录的 Project 我们称之为 root project,子目录的 Project 我们称之为 child project。

项目配置

在根项目里可以对子项目进行配置:

//通过path定位并获取该 Project 对象
project(path: String): Project
//通过path定位一个Project,并进行配置
project(path: String, config: Closure): Project

//针对所有项目进行配置
allprojects(config: Closure)
//针对所有子项目进行配置
subprojects(config: Closure)

我们修改根目录 build.gradle 文件如下:

println "-----root file config-----"

//配置 app 项目
project(":app") {
    ext {
        appParam = "test app"
    }
}

//配置所有的项目
allprojects {
    ext {
        allParam = "test all project"
    }   
}

//配置子项目
subprojects {
    ext {
        subParam = "test sub project"
    }
}

println "allParam = ${allParam}"

修改 app/build.gradle 文件如下:

println "-----app config-----"
println "appParam = ${appParam}"
println "allParam = ${allParam}"
println "subParam = ${subParam}"

修改 library/build.gradle 文件如下:

println "-----library config-----"
println "allParam = ${allParam}"
println "subParam = ${subParam}"

运行结果如下:

-----root file config-----
allParam = test all project
-----app config-----
appParam = test app
allParam = test all project
subParam = test sub project
-----library config-----
allParam = test all project
subParam = test sub project

文件操作

通过mkdir创建目录
File mkDir = mkdir("${buildDir}/test");
File mkDir2 = mkdir("${buildDir}/test2")
println "检测目录是否创建成功:${mkDir.exists()}, ${mkDir2.exists()}"

通过file、files 定位文件
//定位单个文件,参数可以是相对路径、绝对路径
File testDir = file("${buildDir}/test")
println "文件定位是否成功:${testDir.exists()}"

//文件集合,Gradle里用 FileCollection 来表示
FileCollection fileCollection = files("${buildDir}/test", "${buildDir}/test2")
println "-------对文件集合进行迭代--------"
fileCollection.each {File f ->
    println f.name
}
println "-------文件迭代结束-------"
//获取文件列表
Set<File> set = fileCollection.getFiles()
println "文件集合里共有${set.size()}个文件"

通过fileTree创建文件树

Gradle里用 ConfigurableFileTree 来表示文件树,文件树会返回某个目录及其子目录下所有的文件,不包含目录。

//先在build目录下创建3个txt文件
file("${buildDir}/t1.txt").createNewFile()
file("${buildDir}/test/t2.txt").createNewFile()
file("${buildDir}/t1.java").createNewFile()

//1.通过一个基准目录创建文件树,参数可以是相对目录,也可以是绝对目录,与file()方法一样
println "通过基准目录来创建文件树"
ConfigurableFileTree fileTree1 = fileTree("build")
//添加包含规则
fileTree1.include "*.txt", "*/*.txt"
//添加排除规则
fileTree1.exclude "*.java"
fileTree1.each { f ->
    println f    
}

//2.通过闭包来创建文件树
println "通过闭包来创建文件树"
ConfigurableFileTree fileTree2 = fileTree("build") {
    include "*/*.txt", "*.java"
    exclude "*.txt"
}
fileTree2.each { f ->
    println f    
}

//3.通过map配置来创建文件树,可配置的选项有:dir: ''、include: '[]、exclude: []、includes: []、excludes: []
println "通过Map来创建文件树"
def fileTree3 = fileTree(dir: "build", includes: ["*/*.txt", "*.java"])
fileTree3 = fileTree(dir: "build", exclude: "*.java")
fileTree3.each { f ->
    println f    
}

复制文件

复制文件需要使用复制任务(Copy)来进行,它需要指定要复制的源文件和一个目标目录,复制的规则都是定义在 CopySpec 接口里的,更详细的说明可参见 API 文档。

task testCopyFile(type: Copy) {
    //复制build目录下的所有文件
    from "build"
    //复制单独的某个文件
    from "test.java"
    //复制某个文件树下的所有文件
    from fileTree("build")

    include "*.txt"
    include "*.java"
    exclude "t1.txt"
    //指定目标目录
    into "outputs"

    //对复制的文件重命名:通过闭包来映射
    rename { fileName ->
        //增加 rename_ 前缀
        return fileName.endsWith(".java") ? "rename_" + fileName : fileName
    }

    //通过正则来映射文件名:abctest.java 会映射成 abchjy.java
    rename '(.*)test(.*)', '$1hjy$2'
}

删除文件
//删除 build 目录下所有文件
delete("${buildDir}")

下面通过一些例子来解释如何Hook Gradle的构建过程。

  • 为所有子项目添加公共代码

在根项目的build.gradle中添加如下代码:

gradle.beforeProject { project ->
  println 'apply plugin java for ' + project
  project.apply plugin: 'java'
}

这段代码的作用是为所有子项目应用Java插件,因为代码是在根项目的配置阶段执行的,所以并不会应用到根项目中。
这里说明一下Gradle的beforeProject方法和Project的beforeEvaluate的执行时机是一样的,只是beforeProject应用于所有项目,而beforeEvaluate只应用于调用的Project,上面的代码等价于:

allprojects {
  beforeEvaluate { project ->
    println 'apply plugin java for ' + project
    project.apply plugin: 'java'
  }
}

after***也是同理的,但afterProject还有一点不一样,无论Project的配置过程是否出错,afterProject都会收到回调。

  • 为指定Task动态添加Action

gradle.taskGraph.beforeTask { task ->
  task << {
    println '动态添加的Action'
  }
}

task Test {
  doLast {
    println '原始Action'
  }
}

在任务Test执行前,动态添加了一个doLast动作。

  • 获取构建各阶段耗时情况

long beginOfSetting = System.currentTimeMillis()

gradle.projectsLoaded {
  println '初始化阶段,耗时:' + (System.currentTimeMillis() - beginOfSetting) + 'ms'
}

def beginOfConfig
def configHasBegin = false
def beginOfProjectConfig = new HashMap()
gradle.beforeProject { project ->
  if (!configHasBegin) {
    configHasBegin = true
    beginOfConfig = System.currentTimeMillis()
  }
  beginOfProjectConfig.put(project, System.currentTimeMillis())
}
gradle.afterProject { project ->
  def begin = beginOfProjectConfig.get(project)
  println '配置阶段,' + project + '耗时:' + (System.currentTimeMillis() - begin) + 'ms'
}
def beginOfProjectExcute
gradle.taskGraph.whenReady {
  println '配置阶段,总共耗时:' + (System.currentTimeMillis() - beginOfConfig) + 'ms'
  beginOfProjectExcute = System.currentTimeMillis()
}
gradle.taskGraph.beforeTask { task ->
  task.doFirst {
    task.ext.beginOfTask = System.currentTimeMillis()
  }
  task.doLast {
    println '执行阶段,' + task + '耗时:' + (System.currentTimeMillis() - task.beginOfTask) + 'ms'
  }
}
gradle.buildFinished {
  println '执行阶段,耗时:' + (System.currentTimeMillis() - beginOfProjectExcute) + 'ms'
}

将上述代码段添加到settings.gradle脚本文件的开头,再执行任意构建任务,你就可以看到各阶段、各任务的耗时情况。

  • 动态改变Task依赖关系

有时我们需要在一个已有的构建系统中插入我们自己的构建任务,比如在执行Java构建后我们想要删除构建过程中产生的临时文件,那么我们就可以自定义一个名叫cleanTemp的任务,让其依赖于build任务,然后调用cleanTemp任务即可。
但是这种方式适用范围太小,比如在使用IDE执行构建时,IDE默认就是调用build任务,我们没法修改IDE的行为,所以我们需要将自定义的任务插入到原有的任务关系中。

  1. 寻找插入点
    如果你对一个构建的任务依赖关系不熟悉的话,可以使用一个插件来查看,在根项目的build.gradle中添加如下代码:
buildscript {
  repositories {
    maven {
      url "https://plugins.gradle.org/m2/"
    }
  }
  dependencies {
    classpath "gradle.plugin.com.dorongold.plugins:task-tree:1.2.2"
  }
}
apply plugin: "com.dorongold.task-tree"

然后执行gradle <任务名> taskTree --no-repeat,即可看到指定Task的依赖关系,比如在Java构建中查看build任务的依赖关系:

:build
+--- :assemble
|    \--- :jar
|         \--- :classes
|              +--- :compileJava
|              \--- :processResources
\--- :check
     \--- :test
          +--- :classes *
          \--- :testClasses
               +--- :compileTestJava
               |    \--- :classes *
               \--- :processTestResources

我们看到build主要执行了assemble包装任务和check测试任务,那么我们可以将我们自定义的cleanTemp插入到build和assemble之间。

  1. 动态插入自定义任务
    我们先定义一个自定的任务cleanTemp,让其依赖于assemble。
task cleanTemp(dependsOn: assemble) {
  doLast {
    println '清除所有临时文件'
  }
}

接着,我们将cleanTemp添加到build的依赖项中。

afterEvaluate {
  build.dependsOn cleanTemp
}

注意,dependsOn方法只是添加一个依赖项,并不清除之前的依赖项,所以现在的依赖关系如下:

:build
+--- :assemble
|    \--- :jar
|         \--- :classes
|              +--- :compileJava
|              \--- :processResources
+--- :check
|    \--- :test
|         +--- :classes
|         |    +--- :compileJava
|         |    \--- :processResources
|         \--- :testClasses
|              +--- :compileTestJava
|              |    \--- :classes
|              |         +--- :compileJava
|              |         \--- :processResources
|              \--- :processTestResources
\--- :cleanTemp
     \--- :assemble
          \--- :jar
               \--- :classes
                    +--- :compileJava
                    \--- :processResources

可以看到,cleanTemp依赖于assemble,同时build任务多了一个依赖,而build和assemble原有的依赖关系并没有改变,执行gradle build后任务调用结果如下:

:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar UP-TO-DATE
:assemble UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test UP-TO-DATE
:check UP-TO-DATE
:cleanTemp
清除所有临时文件
:build

BUILD SUCCESSFUL

使用Gradle Wrapper来统一构建版本

  • Gradle是个构建系统,能够简化你的编译、打包、测试过程。熟悉Java的同学,可以把Gradle类比成Maven。
  • Gradle Wrapper的作用是简化Gradle本身的安装、部署。不同版本的项目可能需要不同版本的Gradle,手工部署的话比较麻烦,而且可能产生冲突,所以需要Gradle Wrapper帮你搞定这些事情。Gradle Wrapper是Gradle项目的一部分。
  • Android Plugin for Gradle是一堆适合Android开发的Gradle插件的集合,主要由Google的Android团队开发,Gradle不是Android的专属构建系统,但是有了Android Plugin for Gradle的话,你会发现使用Gradle构建Android项目尤其的简单。

另外需要说明的一点是Gradle、Gradle Wrapper与Android Plugin for Gradle不一定要和Android Studio一起使用,你可以完全脱离Android Studio,使用三者独立进行Android项目的构建。下面是三者官方的指导文档(从地址可以看出Gradle Wrapper是Gradle项目的一部分):

  • Gradle: https://docs.gradle.org/current/userguide/userguide_single.html
  • Gradle Wrapper: https://docs.gradle.org/current/userguide/gradle_wrapper.html
  • Android Plugin for Gradle: https://developer.android.com/studio/build/index.html
    可以看到一个gradle/wrapper目录,其中有两个文件:gradle-wrapper.jar/gradle-wrapper.properties,gradle-wrapper.jar是Gradle Wrapper的主体功能包。在Android Studio安装过程中产生gradle-wrapper.jar(如果默认安装的话会在C:\Program Files\Android\Android Studio\plugins\android\lib\templates\gradle\wrapper\gradle\wrapper\gradle-wrapper.jar)。然后每次新建项目,会将gradle-wrapper.jar拷贝到你的项目的gradle/wrapper目录中。gradle-wrapper.properties文件主要指定了该项目需要什么版本的Gradle,从哪里下载该版本的Gradle,下载下来放到哪里,如下图所示:
image

其中GRADLE_USER_HOME一般指~/.gradle,从图示项目中可以知道我要使用gradle-4.1版本,从https://services.gradle.org/distributions/gradle-4.1-all.zip下载,下载到本地的~/.gradle/wrapper/dists目录。那是不是各个项目的Gradle都要通过Gradle Wrapper下载,能不能所有的项目共用一个Gradle?这样理论上是可以的,但是由于Gradle本身不一定保持完全的兼容性,所以新老项目共用一个Gradle有时可能会遇到意想不到的问题。指定对应版本的Gradle,而不通过Gradle Wrapper下载的设置方式是勾选如下图中的Use local gradle distribution,同时指定Gradle home:

image

Gradle对应版本下载完成之后,Gradle Wrapper的使命基本完成了,Gradle会读取build.gradle文件,该文件中指定了该项目需要的Android Plugin for Gradle版本是什么,从哪里下载该版本的Android Plugin for Gradle。如下图所示:

image

从图示项目中可以知道我们要使用3.0.1版本,从google和jcenter处下载,那么下载到我们本地的哪里呢?它会下载到~\.gradle\caches\modules-2\files-2.1\com.android.tools.build中。有时候大家网络装填不好,选择下图中的Offline work时可能出现"No cached version of com.android.tools.build:gradle:xxx available for offline mode"问题,此时你只要将对应版本的Android Plugin for Gradle下载到本地的C:\Program Files\Android\Android Studio\gradle\m2repository\com\android\tools\build中即可。

image

defaultConfig{ } 默认配置,是ProductFlavor类型。它共享给其他ProductFlavor使用

sourceSets{ } 源文件目录设置,是AndroidSourceSet类型。
buildTypes{ } BuildType类型
signingConfigs{ } 签名配置,SigningConfig类型
productFlavors{ } 产品风格配置,ProductFlavor类型
testOptions{ } 测试配置,TestOptions类型
aaptOptions{ } aapt配置,AaptOptions类型
lintOptions{ } lint配置,LintOptions类型
dexOptions{ } dex配置,DexOptions类型
compileOptions{ } 编译配置,CompileOptions类型
packagingOptions{ } PackagingOptions类型
jacoco{ } JacocoExtension类型。 用于设定 jacoco版本
splits{ } Splits类型

A:占位符

在使用友盟进行渠道统计常用的做法是 使用占位符,比如我们可以在AndroidManifest.xml文件使用meta-data进行信息的配置。

image

然后使用的话,如下图:

image

B:签名

签名,是android标签内大家经常使用到的,由于Android 7.0加入了新的签名机制(也就是V2签名)针对这个问题的解决方式可以参考这篇文章 Android-V1、V2签名,因此我们可以直接在debug和release标签中加入v1SigningEnabled true 、v2SigningEnabled true 规避使用风险

下面是加入新签名机制以及原来大家比较熟悉的写法:

image

这里还给大家提供一种关于签名信息的写法(拓展性比第一种较强),首先,我们在app文件的根目录下定义一个文件 signing.properties ,然后写上具体的属性值 (具体的说明如代码截图)

image

由于Gradle是一门脚本,既然是脚本那么它肯定内置了一些函数(注意:Gradle的函数是在app gradle文件内置的标签外 进行编写)给我们操作调用。因为将签名信息写到了这里的配置文件,所以可以通过Gradle去读取信息 然后进行赋值,代码如下:

image

有了读取签名文件的函数,我们就可以进行Alias、password的赋值。

image

C:多渠道包配置

Android Studio给我们提供的多渠道打包方案是使用productFlavors标签配置渠道信息,但是新版本下直接使用这个productFlavors标签会报错,工具要求我们在defaultConfig标签下新增flavorDimensions,如果不使用flavorDimensions关键字,编译会报错:

image

D:自定义apk输出路径

传统的打包操作流程执行编译以及签名后生成的apk默认是 项目 \build\outputs\apk 这个路径下面,那现在我想指定apk输出的文件位置(比如我现在想让这个apk输出到 c盘下面的out_apk文件夹下)该如何操作?

image

其中这里的红色矩形是对上面签名第二种方法的补充说明,这里的buildTypes标签需要写在signingConfigs标签后面,否则很容易编译错误,这个是笔者遇到的问题;蓝色矩形的代码块主要是首先判断是debug还是release版本,如果是release版本就将apk输入到这个指定盘符。

好了,说了这么多gradle文件的配置,下面就开始我们的打包工作。

首先是我们的传统打签名包:

点击Android Studio 顶部Tab , Build —— Generate Sign Apk 然后出现下面的界面:

image

配置好基本信息以后,点击next,勾选V1、V2等一