gradle
Posted 0号凯迪
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了gradle相关的知识,希望对你有一定的参考价值。
Gradle作为一种很方便的构建工具,可以解决:
1. app涉及很多开发者时手动操作带来混乱的问题.
2. Library工程我们需要编译成jar包,用gradle可以自动编译成jar包.
理解构建:
最最简单的构建工具就是make了。make就是根据Makefile文件中写的规则,执行对应的命令,然后得到目标产物。
在Gradle爆红之前,常用的构建工具是ANT,然后又进化到Maven。ANT和Maven这两个工具其实也还算方便,现在还有很多地方在使用。但是二者都有一些缺点,所以让更懒得人觉得不是那么方便。比如,Maven编译规则是用XML来编写的。XML虽然通俗易懂,但是很难在xml中描述if{某条件成立,编译某文件}/else{编译其他文件}这样有不同条件的任务。
Gradle选择了Groovy。Groovy基于Java并拓展了Java。 Java程序员可以无缝切换到使用Groovy开发程序。Groovy说白了就是把写Java程序变得像写脚本一样简单。写完就可以执行,Groovy内部会将其编译成Java class然后启动虚拟机来执行。当然,这些底层的渣活不需要你管。
Gradle另外一个特点就是它是一种DSL,即Domain Specific Language,领域相关语言。
一句行话可以包含很多意思,而且在这个行当里的人一听就懂,不用解释。另外,基于行话,我们甚至可以建立一个模板,使用者只要往这个模板里填必须要填的内容,Gradle就可以非常漂亮得完成工作,得到想要的东西。
Groovy介绍
Groovy是一种动态语言。这种语言比较有特点,它和Java一样,也运行于Java虚拟机中。恩??对头,简单粗暴点儿看,你可以认为Groovy扩展了Java语言。比如,Groovy对自己的定义就是:Groovy是在 java平台上的、具有像Python, Ruby 和 Smalltalk 语言特性的灵活动态语言, Groovy保证了这些特性像 Java语法一样被 Java开发者使用。
除了语言和Java相通外,Groovy有时候又像一种脚本语言。前文也提到过,当我执行Groovy脚本时,Groovy会先将其编译成Java类字节码,然后通过Jvm来执行这个Java类.
实际上,由于Groovy Code在真正执行的时候已经变成了Java字节码,所以JVM根本不知道自己运行的是Groovy代码。
3.6 更多
作为一门语言,Groovy是复杂的,是需要深入学习和钻研的。一本厚书甚至都无法描述Groovy的方方面面。
Anyway,从使用角度看,尤其是又限定在Gradle这个领域内,能用到的都是Groovy中一些简单的知识。
Gradle介绍
现在正式进入Gradle。Gradle是一个工具,同时它也是一个编程框架。前面也提到过,使用这个工具可以完成app的编译打包等工作。当然你也可以用它干其他的事情。
Gradle是什么?学习它到什么地步就可以了?
=====>看待问题的时候,所站的角度非常重要。
-->当你把Gradle当工具看的时候,我们只想着如何用好它。会写、写好配置脚本就OK
-->当你把它当做编程框架看的时候,你可能需要学习很多更深入的内容。
另外,今天我们把它当工具看,明天因为需求发生变化,我们可能又得把它当编程框架看。
Gradle开发环境部署
Gradle的官网:http://gradle.org/
文档位置:https://docs.gradle.org/current/release-notes。其中的User Guide和DSL Reference很关键。User Guide就是介绍Gradle的一本书,而DSL Reference是GradleAPI的说明。
以Ubuntu为例,下载Gradle:http://gradle.org/gradle-download/ 选择Complete distribution和Binary only distribution都行。然后解压到指定目录。
最后,设置~/.bashrc,把Gradle加到PATH里,如图20所示:
执行source ~/.bashrc,初始化环境。
执行gradle --version,如果成功运行就OK了。
注意,为什么说Gradle是一个编程框架?来看它提供的API文档:
https://docs.gradle.org/current/javadoc/org/gradle/api/Project.html
其实gradle就是一个编程框架.写程序就是用gradle的api
从网上学习到的资料来看,几乎全是从脚本的角度来介绍Gradle,结果学习一通下来,只记住参数怎么配置,却不知道它们都是函数调用,都是严格对应相关API的。
掌握大体流程,然后根据SDK+API来完成的
基本组件
Gradle是一个框架,它定义一套自己的游戏规则。我们要玩转Gradle,必须要遵守它设计的规则。
Gradle中,每一个待编译的工程都叫一个Project。每一个Project在构建的时候都包含一系列的Task。比如一个android APK的编译可能包含:Java源码编译Task、资源编译Task、JNI编译Task、lint检查Task、打包生成APK的Task、签名Task等。
一个Project到底包含多少个Task,其实是由编译脚本指定的插件决定。插件是什么呢?插件就是用来定义Task,并具体执行这些Task的东西。
Gradle是一个框架,作为框架,它负责定义流程和规则。而具体的编译工作则是通过插件的方式来完成的。
在例子中
· CPosDeviceSdk、CPosSystemSdk、CPosSystemSdkxxxImpl是Android Library。其中,CPosSystemSdkxxxImpl依赖CPosSystemSdk
· CPosDeviceServerApk和CPosSdkDemo是Android APP。这些App和SDK有依赖关系。CPosDeviceServerApk依赖CPosDeviceSdk,而CPosSdkDemo依赖所有的Sdk Library
每一个Library和每一个App都是单独的Project。根据Gradle的要求,每一个Project在其根目录下都需要有一个build.gradle。build.gradle文件就是该Project的编译脚本,类似于Makefile。
posdevice虽然包含5个独立的Project,但是要独立编译他们的话,得:
1. cd 某个Project的目录。比如 cd CPosDeviceSdk
2. 然后执行 gradle xxxx(xxx是任务的名字。对Android来说,assemble这个Task会生成最终的产物,所以gradle assemble)
所谓的独立Project其实有依赖关系的。
把posdevice改造成支持Gradle的Multi-ProjectsBuild很容易,需要:
· 在posdevice下也添加一个build.gradle。这个build.gradle一般干得活是:配置其他子Project的。比如为子Project添加一些属性。这个build.gradle有没有都无所属。
· 在posdevice下添加一个名为settings.gradle。这个文件很重要,名字必须是settings.gradle。它里边用来告诉Gradle,这个multiprojects包含多少个子Project。
· settings.gradle的内容,最关键的内容就是告诉Gradle这个multiprojects包含哪些子projects:
· [settings.gradle]
·
//通过include函数,将子Project的名字(其文件夹名)包含进来
·
include
'CPosSystemSdk','CPosDeviceSdk',
·
'CPosSdkDemo','CPosDeviceServerApk','CPosSystemSdkWizarPosImpl'
如果你确实只有一个Project需要编译,我也建议你在目录下添加一个settings.gradle。改得方法就是添加settings.gradle,然后include对应的project名字。
settings.gradle除了可以include外,还可以设置一些函数。这些函数会在gradle构建整个工程任务的时候执行,所以,可以在settings做一些初始化的工作
//定义一个名为initMinshengGradleEnvironment的函数。该函数内部完成一些初始化操作
//比如创建特定的目录,设置特定的参数等
definitMinshengGradleEnvironment(){
println
"initialize Minsheng Gradle Environment ....."
......//干一些special的私活....
println
"initialize Minsheng Gradle Environment completes..."
}
//settings.gradle加载的时候,会执行initMinshengGradleEnvironment
initMinshengGradleEnvironment()
//include也是一个函数:
include
'CPosSystemSdk','CPosDeviceSdk',
'CPosSdkDemo','CPosDeviceServerApk','CPosSystemSdkWizarPosImpl'
gradle命令介绍
1. gradle projects查看工程信息
到目前为止,我们了解了Gradle什么呢?
· 每一个Project都必须设置一个build.gradle文件。至于其内容,我们留到后面再说。
· 对于multi-projects build,需要在根目录下也放一个build.gradle,和一个settings.gradle。
· 一个Project是由若干tasks来组成的,当gradle xxx的时候,实际上是要求gradle执行xxx任务。这个任务就能完成具体的工作。
· 当然,具体的工作和不同的插件有关系。编译Java要使用Java插件,编译Android APP需要使用Android APP插件。这些我们都留待后续讨论
如果你修改settings.gradle,使得include只有一个参数,则gradle projects的子project也会变少
gradle tasks查看任务信息
查看了Project信息,这个还比较简单,直接看settings.gradle也知道。那么Project包含哪些Task信息,怎么看呢?图23,24中最后的输出也告诉你了,想看某个Project包含哪些Task信息,只要执行:
gradle project-path:tasks 就行。注意,project-path是目录名,后面必须跟冒号。
对于Multi-project,在根目录中,需要指定你想看哪个poject的任务。不过你要是已经cd到某个Project的目录了,则不需指定Project-path。
· cd CPossystemSdk
· gradle tasks 得到同样的结果
CPosSystemSdk是一个Android Library工程,Android Library对应的插件定义了好多Task。每种插件定义的Task都不尽相同,这就是所谓的Domain Specific,需要我们对相关领域有比较多的了解。
radle task-name执行任务
图25中列出了好多任务,这时候就可以通过 gradle 任务名来执行某个任务。这和make xxx很像。比如:
· gradle clean是执行清理任务,和make clean类似。
· gradle properites用来查看所有属性信息。
gradle tasks会列出每个任务的描述,通过描述,我们大概能知道这些任务是干什么的.....。然后gradle task-name执行它就好。
这里要强调一点:Task和Task之间往往是有关系的,这就是所谓的依赖关系。比如,assemble task就依赖其他task先执行,assemble才能完成最终的输出。
如果知道Task之间的依赖关系,那么开发者就可以添加一些定制化的Task。比如我为assemble添加一个SpecialTest任务,并指定assemble依赖于SpecialTest。当assemble执行的时候,就会先处理完它依赖的task。自然,SpecialTest就会得到执行了...
Gradle工作流程
Gradle的工作流程其实蛮简单,用一个图26来表达:
图26告诉我们,Gradle工作包含三个阶段:
· 首先是初始化阶段。对我们前面的multi-project build而言,就是执行settings.gradle
· Initiliazation phase的下一个阶段是Configration阶段。
· Configration阶段的目标是解析每个project中的build.gradle。比如multi-project build例子中,解析每个子目录中的build.gradle。在这两个阶段之间,我们可以加一些定制化的Hook。这当然是通过API来添加的。
· Configuration阶段完了后,整个build的project以及内部的Task关系就确定了。恩?前面说过,一个Project包含很多Task,每个Task之间有依赖关系。Configuration会建立一个有向图来描述Task之间的依赖关系。所以,我们可以添加一个HOOK,即当Task关系图建立好后,执行一些操作。
· 最后一个阶段就是执行任务了。当然,任务执行完后,我们还可以加Hook。
我在:
· settings.gradle加了一个输出。
· 在posdevice的build.gradle加了图25中的beforeProject函数。
· 在CPosSystemSdk加了taskGraph whenReady函数和buidFinished函数。
Gradle有一个初始化流程,这个时候settings.gradle会执行。
在配置阶段,每个Project都会被解析,其内部的任务也会被添加到一个有向图里,用于解决执行过程中的依赖关系。
然后才是执行阶段。你在gradle xxx中指定什么任务,gradle就会将这个xxx任务链上的所有任务全部按依赖顺序执行一遍!
Gradle编程模型及API实例详解
Gradle主要有三种对象,这三种对象和三种不同的脚本文件对应,在gradle执行的时候,会将脚本转换成对应的对端
· Gradle对象:当我们执行gradle xxx或者什么的时候,gradle会从默认的配置脚本中构造出一个Gradle对象。在整个执行过程中,只有这么一个对象。Gradle对象的数据类型就是Gradle。我们一般很少去定制这个默认的配置脚本。
· Project对象:每一个build.gradle会转换成一个Project对象。
· Settings对象:显然,每一个settings.gradle都会转换成一个Settings对象。
对于其他gradle文件,除非定义了class,否则会转换成一个实现了Script接口的对象当我们执行gradle的时候,gradle首先是按顺序解析各个gradle文件。这里边就有所所谓的生命周期的问题,即先解析谁,后解析谁。
ndk-build命令,用于编译ndk。关于Exec类型的Task,请自行脑补Gradle的API
*/
//注意此处创建task的方法,是直接{}喔,那么它后面的tasks.withType(JavaCompile)
//设置的依赖关系,还有意义吗?Think!如果你能想明白,gradle掌握也就差不多了
task
buildNative(type
:Exec
,description
:'CompileJNI source via NDK'){
if(
project
.gradle
.ndkDir
==null
)//看看有没有指定ndk.dir路径
println
"CANNOT Build NDK"
else{
commandLine
"/${project.gradle.ndkDir}/ndk-build",
'-C',file('jni').
absolutePath
,
'-j',
Runtime
.runtime
.availableProcessors(),
'all','NDK_DEBUG=0'
}
}
tasks
.withType(JavaCompile
){
compileTask
->compileTask
.dependsOn buildNative
}
......
//对于APK,除了拷贝APK文件到指定目录外,我还特意为它们加上了自动版本命名的功能
tasks
.getByName("assemble"){
it
.doLast
{
println
"$project.name: After assemble, jar libs are copied tolocal repository"
project
.ext
.versionName
=android
.defaultConfig
.versionName
println
"\t versionName = $versionName"
copyOutput(false)
}
}
结果展示
在posdevice下执行gradle assemble命令,最终的输出文件都会拷贝到我指定的目录,结果如图38所示:
图38所示为posdevice gradle assemble的执行结果:
· library包都编译release版的,copy到xxx/javaLib目录下
· apk编译debug和release-unsigned版的,copy到apps目录下
· 所有产出物都自动从AndroidManifest.xml中提取versionName
--我会把它改造成支持Multi-Projects Build的样子。即在工程目录下放一个settings.build。
它有三个版本,分别是debug、release和demo。这三个版本对应的代码都完全一样,但是在运行的时候需要从assets/runtime_config文件中读取参数。参数不同,则运行的时候会跳转到debug、release或者demo的逻辑上。
在编译build、release和demo版本前,在build.gradle中自动设置runtime_config的内容。代码如下所示:
[build.gradle]
apply plugin
:'com.android.application'//加载APP插件
//加载utils.gradle
apply from
:rootProject
.getRootDir().getAbsolutePath()+"/utils.gradle"
//buildscript设置android app插件的位置
buildscript
{
repositories
{jcenter()}
dependencies
{classpath
'com.android.tools.build:gradle:1.2.3'}
}
//androidScriptBlock
android
{
compileSdkVersion gradle
.api
buildToolsVersion
"22.0.1"
sourceSets
{//源码设置SB
main
{
manifest
.srcFile
'AndroidManifest.xml'
jni
.srcDirs
=[]
jniLibs
.srcDir
'libs'
aidl
.srcDirs
=['src']
java
.srcDirs
=['src']
res
.srcDirs
=['res']
assets
.srcDirs
=['assets']//多了一个assets目录
}
}
signingConfigs
{//签名设置
debug
{//debug对应的SB。注意
if(
project
.gradle
.debugKeystore
!=null
){
storeFile
file("file://${project.gradle.debugKeystore}")
storePassword
"android"
keyAlias
"androiddebugkey"
keyPassword
"android"
}
}
}
/*
最关键的内容来了: buildTypesScriptBlock.
buildTypes和上面的signingConfigs,当我们在build.gradle中通过{}配置它的时候,
其背后的所代表的对象是NamedDomainObjectContainer<BuildType> 和
NamedDomainObjectContainer<SigningConfig>
注意,NamedDomainObjectContainer<BuildType/或者SigningConfig>是一种容器,
容器的元素是BuildType或者SigningConfig。我们在debug{}要填充BuildType或者
SigningConfig所包的元素,比如storePassword就是SigningConfig类的成员。而proguardFile等
是BuildType的成员。
那么,为什么要使用NamedDomainObjectContainer这种数据结构呢?因为往这种容器里
添加元素可以采用这样的方法:比如signingConfig为例
signingConfig{//这是一个NamedDomainObjectContainer<SigningConfig>
test1{//新建一个名为test1的SigningConfig元素,然后添加到容器里
//在这个花括号中设置SigningConfig的成员变量的值
}
test2{//新建一个名为test2的SigningConfig元素,然后添加到容器里
//在这个花括号中设置SigningConfig的成员变量的值
}
}
在buildTypes中,Android默认为这几个NamedDomainObjectContainer添加了
debug和release对应的对象。如果我们再添加别的名字的东西,那么gradleassemble的时候
也会编译这个名字的apk出来。比如,我添加一个名为test的buildTypes,那么gradle assemble
就会编译一个xxx-test-yy.apk。在此,test就好像debug、release一样。
*/
buildTypes
{
debug
{//修改debug的signingConfig为signingConfig.debug配置
signingConfig signingConfigs
.debug
}
demo
{//demo版需要混淆
proguardFile
'proguard-project.txt'
signingConfig signingConfigs
.debug
}
//release版没有设置,所以默认没有签名,没有混淆
}
......//其他和posdevice 类似的处理。来看如何动态生成runtime_config文件
def
runtime_config_file
='assets/runtime_config'
/*
我们在gradle解析完整个任务之后,找到对应的Task,然后在里边添加一个doFirst Action
这样能确保编译开始的时候,我们就把runtime_config文件准备好了。
注意,必须在afterEvaluate里边才能做,否则gradle没有建立完任务有向图,你是找不到
什么preDebugBuild之类的任务的
*/
project
.afterEvaluate
{
//找到preDebugBuild任务,然后添加一个Action
tasks
.getByName("preDebugBuild"){
it
.doFirst
{
println
"generate debug configuration for ${project.name}"
def
configFile
=newFile(runtime_config_file
)
configFile
.withOutputStream
{os
->
os
<<I am Debug\n'
//往配置文件里写 I am Debug
}
}
}
//找到preReleaseBuild任务
tasks
.getByName("preReleaseBuild"){
it
.doFirst
{
println
"generate release configuration for ${project.name}"
def
configFile
=newFile(runtime_config_file
)
configFile
.withOutputStream
{os
->
os
<<I am release\n'
}
}
}
//找到preDemoBuild。这个任务明显是因为我们在buildType里添加了一个demo的元素
//所以Android APP插件自动为我们生成的
tasks
.getByName("preDemoBuild"){
it
.doFirst
{
println
"generate offlinedemo configuration for${project.name}"
def
configFile
=newFile(runtime_config_file
)
configFile
.withOutputStream
{os
->
os
<<I am Demo\n'
}
}
}
}
}
.....//copyOutput
最终的结果如图39所示:
为什么我知道有preXXXBuild这样的任务?
答案:gradle tasks --all查看所有任务。然后,多尝试几次,直到成功
· 其是Groovy语法,虽然看了下快速教材,但总感觉一到gradle就看不懂。主要问题还是闭包,比如Groovy那一节写得文件拷贝的例子中的withOutputStream,还有gradle中的withType,都是些啥玩意啊?
· 所以后来下决心先把Groovy学会,主要是把自己暴露在闭包里边。另外,Groovy是一门语言,总得有SDK说明吧。写了几个例子,慢慢体会到Groovy的好处,也熟悉Groovy的语法了。
· 接着开始看Gradle。Gradle有几本书,我看过Gradle in Action。说实话,看得非常痛苦。现在想起来,Gradle其实比较简单,知道它的生命周期,知道它怎么解析脚本,知道它的API,几乎很快就能干活。而Gradle In Action一上来就很细,而且没有从API角度介绍。说个很有趣的事情,书中有个类似下面的例子
taskmyTask << {
println ' I am myTask'
}
书中说,如果代码没有加<<,则这个任务在脚本initialization(也就是你无论执行什么任务,这个任务都会被执行,I am myTask都会被输出)的时候执行,如果加了<<,则在gradle myTask后才执行。
不要这么阐释问题.理解的话直接说为什么.然后就可以了.
这和我们调用task这个函数的方式有关!如果没有<<,则闭包在task函数返回前会执行,而如果加了<<,则变成调用myTask.doLast添加一个Action了,自然它会等到grdle myTask的时候才会执行
哈哈,差不多把人家所有的东西都搬过来了,写的确实好.感恩.
以上是关于gradle的主要内容,如果未能解决你的问题,请参考以下文章
如何使用模块化代码片段中的LeakCanary检测内存泄漏?
如何防止Android studio格式化build.gradle