Gradle是一款专注于灵活性和性能的开源构建自动化工具。 Gradle构建脚本使用 Groovy 或 Kotlin DSL 编写,具有以下特点:
高度可定制 — Gradle以最基本的方式进行定制和扩展
快速 — Gradle通过重复使用先前执行的输出,仅处理已更改的输入以及并行执行任务来快速完成任务
强大 — Gradle是Android的官方构建工具,并且支持许多流行的语言和技术
安装 一行命令搞定法:
1 2 3 $ brew install gradle $ gradle -v
基础 Projects & tasks 任何一个 Gradle 构建都是由一个或多个 projects 组成,每个 task 都代表了构建执行过程中的一个原子性操作。如编译,打包,生成 javadoc,发布到某个仓库等操作
Hello World
1 2 3 4 5 6 7 task hello { doLast { println 'hello world' } }
首先定义了一个叫做 hello
的 task
doLast - Adds the given closure to the end of this task’s action list,可以从官方文档中看的出来,doLast 把参数中的闭包放到了所有 task 的最后执行
其中 -q
参数表示只打印错误信息,不会打印其他日志信息
需要注意的是下面 task 的写法:
1 2 3 task why { println "hello" }
这种写法属于给当前文件中的任务增加共同行为
快速定义
1 2 3 task append << { println 'hello world' }
<<
操作符不能理解为 Groovy 中的追加操作,必须理解为 doLast
的简单写法。需要的注意的是 doLast
默认会按顺序执行所在文件中所有的 task
任务依赖 任务依赖主要用 dependsOn
,有两种用法,第一种为常规用法:
1 2 3 4 5 6 7 8 9 10 11 12 task append << { println 'append' } task depend(dependsOn: append ) << { println 'depend' } task compile (dependsOn: [append , denpend]) << { println 'compile' }
第二种是当被依赖的 task 声明在当前 task 之后的写法:
1 2 3 4 5 6 7 8 task depend(dependsOn: 'append' ) << { println 'depend' } task append { println 'append' }
动态创建任务 使用 Groovy 循环方式也可以创建 task:
1 2 3 4 5 1 .upto (5 ){ task "task_$it" << { println "$it" } }
使用已存在的 task
1 2 3 4 5 6 7 8 9 10 11 12 13 task_1.dependsOn task_2, task_3 task_1.doFirst { println "do $task_1.name" } task_1.doLast { println "finish $task_1.name" } task_1 << { println "finish $task_1.name again" }
task 属性
1 2 3 4 5 6 7 8 9 10 11 12 13 task_1 << { println "finish $task_1.name again" } task config { ext.customName = 'kevin' } task test << { println config.customName }
默认 task 使用 defaultTasks
定义默认 task,多个 task 使用逗号区分:
1 defaultTasks 'append' , 'hello'
为不同 task 配置不同参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 File [] getFiles(dir) { file (dir).listFiles({file -> file .isFile()} as FileFilter).sort () } task loadDirFile << { def files = getFiles("$dir" ) if (files.length > 0 ) { files.each { file -> ant .loadfile(srcFile: file , property: file .name) println "${ant.properties[file.name]}" } } } task checkSum << { def files = getFiles("$dir" ) if (files.length > 0 ) { files.each { file -> ant .checksum(file : file , property: file .name) println "${ant.properties[file.name]}" } } } gradle.taskGraph.whenReady { task -> if (task .hasTask(checkSum)) { ext.dir = ".." } else if (task .hasTask(loadDirFile)) { ext.dir = "." } }
Gradle Wrapper 运行 Gradle Script 的推荐方式就是使用 Gradle Wrapper,先看一下 Gradle Wrapper 的工作流程:
Wrapper是一个调用Gradle声明版本的脚本,如果需要的话可以事先下载。因此,开发人员可以快速启动并运行Gradle项目,不用担心本地 Gradle 版本和项目中使用的 Gradle 版本不一致导致一些不可预见的问题,从而节省公司的时间和金钱。
Gradle 命令行 执行 gradle -h
可以查看所有参数以及描述
多任务调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 task compile << { println 'compile' } task compileTest(dependsOn: compile ) << { println 'compileTest' } task runTest(dependsOn: [compile , compileTest]) << { println 'runTest' } task running(dependsOn: runTest) << { println 'running' }
执行
gradle running compile
后的结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 ➜ gradle running compile > Task :compile compile > Task :compileTest compileTest > Task :runTest runTest > Task :running running
这是因为每个 task 只会执行一次,running
已经依赖了 compile
,所以 compile
不会执行两次
排除任务
-x, –exclude-task Specify a task to be excluded from execution. 指定一个 task 不执行
1 2 3 4 5 6 7 8 9 10 ➜ gradle running -x compile > Task :compileTest compileTest > Task :runTest runTest > Task :running running
失败后继续执行
–continue Continue task execution after a task failure. 当一个 task 失败后继续往后执行
简化任务名 需要执行任务时,无须输入全部任务名,只需提供足够的可以唯一区分出该任务的字符即可
指定构建文件
-b, –build-file Specify the build file. 指定一个构建文件
获取任务信息
tasks - Displays the tasks runnable from root project ‘gradle’ 获取任务新
1 2 3 4 $ gradle -q tasks --all $ gradle help --task taskName
配置任务参数
1 2 3 4 5 task hello { description = "hello" version = "10.0.0" group = "build" }
获取依赖信息 针对 Android 项目的一个例子:
1 2 3 4 5 6 $ gradle -q projects $ gradle -q app:dependencies lib:dependenices $ gradle buildEnvironment
获取指定依赖信息 首先需要明白 configuration
的概念:
What is a configuration?
Every dependency declared for a Gradle project applies to a specific scope. For > example some dependencies should be used for compiling source code whereas others only need to be available at runtime. Gradle represents the scope of a dependency with the help of a Configuration. Every configuration can be identified by a unique name.
Many Gradle plugins add pre-defined configurations to your project. The Java plugin, for example, adds configurations to represent the various classpaths it needs for source code compilation, executing tests and the like. See the Java plugin chapter for an example. The sections above demonstrate how to declare dependencies for different use cases.
上面是官方的解释,简单翻译后可以理解为:
每个 Gradle 项目的依赖都有一个特定的范围,比如 implementation
以及 api
,根据这些配置,Gradle 才能知道该怎么处理这些依赖。其实 implementation
以及 api
都是 Android 项目预定义的 configuration
1 2 $ gradle app:dependencies --configuration implementation
获取特定依赖报告
1 2 3 $ gradle -q app:dependencyInsight --dependency com.facebook.stetho --configuration compileClasspath
dependencyInsight
可以查看依赖深度报告,用来排查依赖冲突非常有效
获取属性
1 $ gradle -q app:properties
构建日志 加上 --profile
参数就可以在根目录的 build/reports/profile
中生成一个以日期命名的日志文件:
1 $ gradle -q --profile getConfigurationByName
Dry Run 只想知道某个任务在一个任务集中按顺序执行的结果,但并不想实际执行这些任务,可以使用 -m
参数:
给 JVM 添加参数
1 $ gradle -Dmyprop=myprop
Gradle 的
-D 参数和 Java 的
-D 参数功能一致
Gradle DSL DSL 的全称就是 Domain Specific Language(领域特定语言),通过例子可能会更好的理解:
正则表达式 - 按照指定规则编写字符串,是否匹配全交给了语言引擎来完成
SQL - 可以看作数据库领域的 DSL
Markdown - 写作领域的 DSL
声明变量 局部变量 只在定义该变量的文件中有效的变量:
1 2 3 4 5 6 def value = 'kevin' task getValue { println value }
额外属性 额外属性可以通过所属对象的 ext
属性进行添加,读取和设置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 // root/build.gradle ext { value = 'world' } ext.a = 'a' ext.b = [1,2,3] ext.c = [:] c.a = 'c.a' c.b = 'c.b' // root/app/buidl.gradle def value = 'kevin' task getValue << { println "Local variable is $value, Global variable is $c.a" }
同时也可以使用
gradle.properties
文件为
project
对象添加额外参数,如果当前目录中不存在配置文件,读取项目根目录中的配置文件
-P
参数设置属性:
1 2 3 4 5 task getCommandValue << { println commandValue }
-D
参数设置属性:
1 2 3 4 5 task getSystemValue << { println System.getProperty("systemValue" ) }
通过系统变量设置属性,当有类似 ORG_GRADLE_PROJECT_propName=value
这样格式的系统变量时,Gradle 里面就可以使用 propName
属性,这样就可以设置一个:
1 2 3 4 5 6 7 task getSystemValue2 << { println value }
还有一种情况是需要设置系统变量时,就可以通过 gradle.properties
文件来设置,如果该文件中有一个 systemProp.
为前缀的属性,该属性和它对应的值就会被添加到系统属性中,且不带前缀。需要注意的是,在多项目构建中,除了根目录的 systemProp.
属性,其他目录下的都将被忽略:
1 2 3 4 5 6 7 8 // gradle.properties systemProp.helloWorld=hello // build.gradle // command: gradle -q getSystemValue // output: hello task getSystemValue << { println System.getProperty("helloWorld") }
如果同时在 gradle.properties
和命令行中都设置了同一个参数,则优先使用命令行中配置的参数
闭包委托 修炼中…
使用外部构建脚本
1 2 3 4 5 6 7 8 9 10 11 ext.otherValue='other' ext.otherMethod={ println "other.gradle#otherMethod" } apply from : 'other.gradle' task getOtherScriptValue << { otherMethod() println otherValue }
缓存 Gradle 默认缓存所有编译过的脚本,如果有脚本有改动重新编译并缓存,没有改动则使用缓存不进行编译。使用 --recompile-scripts
选项运行 Gradle 会强制丢弃缓存重新编译并缓存
Task 定义任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 task myTask{ println 'myTask' } task (myTask){ println 'myTask' } task ('myTask' ){ println 'myTask' } task (myTask, type: Copy ){ from "." into ".." include '*.gradle' } tasks.create(name: 'createTask' ) { println 'crate task' }
重写任务
1 2 3 4 5 6 7 8 9 10 tasks.create(name: 'createTask' ) << { println 'crate task' } task createTask(overwrite: true ) << { println 'overwrite' }
必须把
overwrite
属性设置为
true ,否则会抛出
a task with that name already exists
的异常
自定义类型 Task
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class CustomTask extends DefaultTask { String value = 'hello workd' @TaskAction def init() { println "i'm custom task -- $value" } } task customTask1(type: CustomTask)task customTask2(type: CustomTask) { value = 'custom prop' }
该任务添加一个方法并使用 @TaskAction
注释标记它。当任务执行时,Gradle将调用该方法
跳过任务 onlyIf Typical usage:myTask.onlyIf{ dependsOnTaskDidWork() }
,当闭包中的内容返回 true 时,才会执行该 Task:
1 2 3 4 5 6 7 8 createTask.onlyIf { project .hasProperty('skip' ) }
StopExecutionException
1 2 3 4 5 6 7 8 9 10 11 12 task compile << { println 'We are doing the compile.' } compile .doFirst { if (true ) { throw new StopExecutionException() } } task myTask(dependsOn: 'compile' ) << { println 'I am not affected' }
Task enable 属性 每个 task 都有一个值为 true 的 enable 属性,如果把这个属性设置为 false,则不会执行该 task 的任何操作
任务规则
1 2 3 4 5 6 7 8 9 10 tasks.addRule("Pattern: ping<ID>" ) { String taskName -> if (taskName.startsWith("ping" )) { task (taskName) << { println "Pinging: " + (taskName - 'ping' ) } } }
conventions vs extensions 扩展是较新的概念,已在很大程度上取代了约定。简而言之,仅使用扩展名,而不使用约定。
日志 等级
Level
Feature
ERROR
错误消息
QUIET
重要的信息消息
WARNING
警告
LIFECYCLE
进度
INFO
信息性消息
DEBUG
调试消息
日志命令参数
选项
输出
无
LIFECYCLE 及更高
–quiet
QUIET 及更高
–info
INFO 及更高
–debug
DEBUG 及更高
堆栈跟踪参数
选项
含义
无
构建错误(如编译错误)时没有栈跟踪打印到控制台。只有在内部异常的情况下才打印栈跟踪。如果选择 DEBUG 日志级别,则总是输出截取后的栈跟踪信息
–stacktrace
输出截断的栈跟踪。我们推荐使用这一个选项而不是打印全栈的跟踪信息。Groovy 的全栈跟踪非常冗长 (由于其潜在的动态调用机制,然而他们通常不包含你的的代码中哪里错了的相关信息)
–full-stacktrace
打印全栈的跟踪信息
打印日志
1 2 3 4 5 6 7 8 prinln 'log' logger.quiet('An info log message which is always logged.' ) logger.error('An error log message.' ) logger.warn('A warning log message.' ) logger.lifecycle('A lifecycle info log message.' ) logger.info('An info log message.' ) logger.debug('A debug log message.' ) logger.trace('A trace log message.' )
自定义 configuration
1 2 3 4 5 6 7 8 9 10 11 12 13 configurations { custom } dependencies { custom 'com.facebook.stetho:stetho:1.5.0' } task copyDependencies(type: Copy ){ from configurations .custom into 'build/bins' }
可以根据自定义的 configurations
做一些不一样的事情,比如把自定义 configurations
所依赖的库拷贝到指定的文件夹中
debug plugin 只用改一个你想要的名字即可,其他参数不用变:
下好断点位置后,执行如下指令:
1 $ ./gradlew assembleDebug -Dorg.gradle.daemon=false -Dorg.gradle.debug=true
看到这个提示后再执行 debug 操作:
自定义插件 参考资料 参考资料 官方 dsl demo kts 配置 uploadArchives 中文 demo
小技巧 忽略debug版本
1 2 3 4 5 variantFilter { variant -> if (variant.buildType.name == 'debug' ) { setIgnore(true ) } }
设置 apk 文件名称
1 2 3 4 5 android.applicationVariants.all { variant -> variant.outputs.all { outputFileName = "${defaultConfig.applicationId}-${variant.productFlavors[0].name}.apk" } }
util.gradle 总结了常用的一些工具函数 util.gradle
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 ext.getCurrentFlavor = { Gradle gradle = getGradle() String tskReqStr = gradle.getStartParameter().getTaskRequests().toString() Pattern pattern if (tskReqStr.contains("assemble" )) pattern = Pattern.compile ("assemble(\\w+)(Release|Debug)" ) else pattern = Pattern.compile ("generate(\\w+)(Release|Debug)" ) Matcher matcher = pattern.matcher(tskReqStr) if (matcher.find ()) { println matcher.group (1 ).toLowerCase() return matcher.group (1 ).toLowerCase() } else { println "NO MATCH FOUND" return "" } } ext.getCurrentBuildType = { Gradle gradle = getGradle() String tskReqStr = gradle.getStartParameter().getTaskRequests().toString() if (tskReqStr.contains("Release" )) { println "getCurrentBuildType release" return "release" } else if (tskReqStr.contains("generateDebug" )) { println "getCurrentBuildType debug" return "debug" } println "NO MATCH FOUND" return "" }
build.gradle
中使用:
1 2 3 4 apply from : 'util.gradle' task callOtherScriptMethod { getCurrentBuildType() }
实例 讲了这么多,下面列举几个常见的 Gradle 配置:
加速Gradle构建 阿里国内镜像,下载速度很快,从此告别添加依赖后再等上十分钟
1 2 3 4 5 allprojects { repositories { maven{ url 'http://maven.aliyun.com/nexus/content/groups/public/'} } }
修改默认 apk 文件名 很多时候需要通过 apk 的命名很直观的看到一些信息,比如版本号,渠道以及是否测试版等等,此时就需要修改 apk 文件名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 buildTypes { ... applicationVariants.all { variant -> variant.outputs.each { output -> def outputFile = output.outputFile if (outputFile != null && outputFile.name.endsWith('.apk' )) { def fileName = "LoveDev-v${defaultConfig.versionName}.apk" output.outputFile = new File (outputFile.parent, fileName) } } } ... }
自动生成 versionCode 和 versionName
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 static def getVersionCode(boolean isDebug) { if (isDebug) { return Integer.parseInt(new Date().format("yyMMddHHmm" )) } return getRevisionNumber() } def getVersionName(boolean isDebug) { String version = rootProject.ext.appmajor + '.' + rootProject.ext.appminor + '.' + getRevisionNumber() String today = new Date().format('yyMMdd' ) String time = new Date().format('HHmmss' ) if (isDebug) { return version + ".$today.$time." + getRevisionDescription() } return version + ".$today." + getRevisionDescription() } static def getRevisionNumber() { Process process = "git rev-list --count HEAD" .execute() process.waitFor() return process.getText ().toInteger () } static def getRevisionDescription() { String desc = 'git describe --always' .execute().getText ().trim() return (desc == null || desc.size () == 0 ) ? new Date().format("yyMMdd" ) : desc.substring(desc.size () - 6 ) }
productFlavors 配置产品风味 如果除了正式版,测试版之外还要继续细分化,针对不同的渠道,正式版要分为收费版,免费版等情况,这时候就需要用到 productFlavors
来配置了
将配置信息添加到 productFlavors {}
代码块并配置想要的设置。产品风味支持与 defaultConfig
相同的属性,这是因为 defaultConfig
实际上属于 ProductFlavor
类,我在实际项目中碰到一个需求,针对某一个功能方法,分为两个版本处理,除此之外在 CI 环境中还要同时上传这两个版本,这就意味着两个版本的包名也要不同,下面是示例代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 productFlavors { shine { buildConfigField "int" , "AUDIO_VERSION" , "2" versionCode 2 } webrtc { buildConfigField "int" , "AUDIO_VERSION" , "1" versionCode 1 } } buildTypes { ... applicationVariants.all { variant -> variant.outputs.each { output -> def outputFile = output.outputFile if (outputFile != null && outputFile.name.endsWith('.apk' )) { def fileName = "LoveDev-v${defaultConfig.versionName}-${variant.productFlavors[0].name}.apk" output.outputFile = new File (outputFile.parent, fileName) } } } ... }
配置之后在
buildType
类中就有了int 类型的
AUDIO_VERSION
字段,可以根据该字段判断方法中的具体实现
其实很多配置,在官网里面已经提到过了,不过还是要根据具体的项目进行具体的配置,尽量不要照抄照搬,官网地址请戳这里
sourceSets 设置源集 如果需要为一个程序在逻辑不变的情况下配置多套界面,就需要用到 sourceSets
来配置项目源集,现在有如下结构的一个程序:
1 2 3 4 +--main | +--java | +--skin-custom | +--res
其中
skin-custom
存放的就是定制皮肤,
build.gradle
需要进行如下配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 flavorDimensions("skin" ) productFlavors { standard { dimension "skin" } skin { dimension "skin" } } sourceSets { standard { res.srcDirs = ['src/main/res' ] } skin { res.srcDirs = ['src/main/skin-custom' ] } }
如果在
skin-custom
中获取不到需要的文件,就遍历其他文件夹寻找该文件
忽略 release buildType
1 2 3 4 5 6 7 8 9 10 android{ ... variantFilter { variant -> def names = variant.buildType.name if (names.contains("release" )) { setIgnore(true ) } } ... }
生成 jar 包
1 2 3 4 5 6 7 8 task makeJar(type: Copy ) { delete 'build/myapp.jar' from ('build/intermediates/packaged-classes/release/' ) into ('build/' ) include ('classes.jar' ) rename('classes.jar' , 'myapp.jar' ) } makeJar.dependsOn(build)
评论