前话:
最近谷歌宣布官方不再维护Eclipse ADT了,之后将更加专注于Android Studio的功能和性能上的改进,早在2013年的Google IO大会上首次推出了Android Studio,当时刚出来的时候我就好奇的去下载体验了一下,想看一下新开发工具的优势在哪里,据官方介绍,最吸引我的一点就是使用Studio使用了Gradle编译系统,可以支持很灵活的定制需求,而我当时正在研究当成库使用的APK(就是现在的aar文件,不过当时还没有出身),刚好遇到了ADT编译系统的限制,所以当时看到Studio非常兴奋,于是我当时就进行过一系列的研究,包括Gradle,Groovy,Aant,Maven,不过当时太懒没有留下文章只是做了一些笔记。我曾经也试着在自己公司推广Gradie,但当时同事们还是不太愿意去额外学习一个工具,觉得Eclipse够用,然后项目组也觉得有风险,所以当时就把这个事放下了,直到今年,Google大力推广Studio,还把ADT直接从Android官方下了,当前项目组也因为只在Studio所支持的multi-dex特性而被迫迁移了到了Studio,其实当时集体迁,我还是觉得有点风险,不过迁了之后发现并没想像中那么麻烦,甚至非常简单,因为可以让同一份代码同时支持Eclipse编译和Gradle编译,当然,这不是Google官方所建议的,但却是最受同事欢迎的,这样可以无缝迁移,而且迁移工作也很简单,就是在每个Eclipse工程(包括主工程和库工程)目录下放一个build.gradle就可以,具体做法,我写到另外一篇文章中吧(等我闲的时候写)。因为不得不迁移,所以我写下这篇文章,希望帮助新上手同学理解Gradle,使大家可以看懂Gradle构建脚本,并且能定制一些简单的个性化编译需求。
构建工具的的展:
大多数介绍gradle的文章都会写到:Gradle既有Ant的强大和灵活性,又有Maven的易用性。ant和maven是什么,也许你没听过,也许你是那个领域的专家,简单来说,他们都构建工具,构建是英文build的翻译,所以,何谓构建工具,如果你一直使用IDE作为开发工具,可能会不太清楚,因为IDE已经帮你把所有的活干了(我不是反对用IDE,而是觉得可以去了解一下IDE的内部流程),构建工具不同于编译工具,他是用于组织编译、单元测试、发布等操作,并且简化这些操作,构建工具与编译工具的关系是构建工具调用了编译工具,每当你执行一次构建操作的时候,内部实际自动执行了编译、单元测试,发布等操作。也许你会说为什么要构建工具,我写个脚本不就行了,我第一个学习构建工具——Makefile的时候也是这么想的,如果只是用于组织编译步骤,写个脚本确实简单得多,不过构建工具并不是简单的调用编译等操作,他还要提高效率和节省资源,比如当你第二次执行构建时,如果源代码没有任何修改,构建工具应该聪明的跳过编译操作,直接使用上一次的编译成果,如果你的源代码只有部分修改,那么构建工具应该仅部分编译修改过的内容。也许睿智的你会立马想到,我在脚本里加个If判断也行啊,你当然可以那样实现,但随时着项目规模的扩大,那样的脚本复杂度会呈指数型上升,直接你的自己都不着维护那么脚本,一旦有新的编译需要,那将会是你的噩梦。构建工具诞生就是为了优雅解决这些问题,有了构建工具之后,写一个简洁的构建脚本,便可以轻松的应对这一切。
在详细介绍Gradle之前,我们先来细数一下构建工具的发展吧,最初最元老的构建工具当然算Makefile了,Makefile的强大让他驰骋了几十年,至今仍是Linux上C/C++开发最流行的构建工具,上G级别的Android系统开源项目就是由Makefile构建的,不但强大,Makefile脚本还很简单易用,上手快,Makefile脚本就是包含一系列规则,每条规则包含一个目标、依赖和命令,每个目标对应一些依赖和一串命令,一个目标是将命令作用于他的依赖上生成的,比如你用C写了个helloworld.c,你可以写一个目标为helloworld,他的依赖是helloworld.c,他的命令是gcc helloworld.c -o helloworld。即这个简单Makefile脚本就仅包含一条规则,内容如下:
helloworld: helloworld.c
gcc helloworld.c -o helloworld
构建之后,会生成名为helloworld的可执行文件,每次你执行构建的时候,Makefile会比较helloworld和helloworld.c,看哪个新,如果helloworld.c新就运行命令"gcc helloworld.c -o helloworld"重新生成helloworld,否则直接结束。不过,真实情况下,往往会有很多目标和依赖,一个目标(对象A)的依赖(对象B)还可能依融另一个对象C,比如你的可执行程序(对象A),依融某库(对象B),而对象B又靠一个代码文件(对象C)来生成,这时你就要写两条这样的规则了,大致如下:
对象A: 对象B
命令...
对象B: 对象C
命令...
大项目往往有很多条规则,于是就形成了树形的依赖链,Makefile就递归的对比目标和依赖新旧来决定某条链是否要重新生成。虽然Makefile不只一种规范,但大同小异,其中以
GNU Make最流行。
Makefile的原理可以让我们更好的理解更高级的构建工具,所以长篇大论了这么久。Makefile出来之后,有一段很长的统治时期,直到Java出世,Java是为跨平台而生,而Makefile成了一个大大的绊脚石,所以Java开发人员急切的需要一个跨平台的构建工具,最好能在JVM上运行,于是Ant诞生了,不可否定,Ant有很多思想来自于Makefile,虽然有很多改进。在我看来,Ant构建脚本相比Makefile脚本更简单了,不过可能要长一点,因为Ant使用了XML文件格式,不过无关紧要,XML文件只是众多能承载树形结形的载体格式之一,如果你愿意,可以开发个使用JSON格式文件的Ant,正因为Ant构建脚本的思想更简单了,所以很多人更愿意叫他为构建配置文件,把他当成一个用XML呈现的配置文件,脚本一般是指一连串可执行的命令,配置文件一般是指能被程序扫描成结构化的数据体,所以你可以认为Ant执行一个构建脚本来做一次构建,也可以认为Ant扫描了一个配置文件,根据其配置项做了一次构建,都说得过去。Ant脚本名称为build.xml,一个Ant脚本包括project、target、task、propert四大元素,其中target跟Makefile的含义基本一样,每个构建脚本只包一个project,至少一个target,每个target包含若于task,每个task相当于一个命令,如mkdir,在Ant执行阶段会实例化ant.jar里的一个Task的子类,每个target有若干个Attribute,有的是必要的,有的非必要,是执行该命令是需要的参数,propert可以先不管它,相当于一个变量。一个简单的helloworld的Ant脚本如下:
<project>
<target name="compile">
<mkdir dir="build/classes"/>
<javac srcdir="src" destdir="build/classes"/>
</target>
<target name="jar" depends="compile">
<mkdir dir="build/jar"/>
<jar destfile="build/jar/HelloWorld.jar" basedir="build/classes"/>
</target>
</project>
简单的说,Ant就是一系列target和task,也正因为与Makefile思想的相近性,使得用Ant编译C项目也是很简单的,不过没必要这么干,还要装个JVM,多麻烦。
Ant出现之后,为很多Java开发人员带来福音,不过随着软件行业的日益发展,软件规模越来越大,大家开始慢慢的发现Ant不够用,一方面是觉得不同项目存在很多相同的构建流程,但每开一个新项目,不得不重复写一遍那些流程,比如大部分项目的构建流程都是编译、单元测试,打包、发布,所以是人们希望构建工具内部将这一部分共同的东西固化下来以便复用,来加速新项目的周期,特别是中小项目;另一方面,人们发现,一个项目往往依赖很多其它项目,其它项目又依赖其它项目,为了构建,人们不得不重复的拷贝这些库项目,同时开发库项目的人也不能把最新的版本快速的推广出去,应用周期很多长。为了解决这一系列问题,Maven出世了,Maven与之前的构建工具有极大的区别,虽然他使用XML文件格式(pom.xml),首先他固化了构建流程,他认为构建过程是基本不变的,在Maven中称为标准构建生命周期,只是其中的某些步骤需要定制,所以Maven的构建脚本文件更应该称为构建配置文件,其中的配置项定制了子步骤的一些属性;其次Maven约定了一套工程目录结构,假如你使用(也强制建议使用)这套目录结构,你使用极少的配置就可以构建好你的工程,这个目录结构大概如下:
my-app
|-- pom.xml
`-- src
|-- main
| `-- java
| `-- com
| `-- mycompany
| `-- app
| `-- App.java
`-- test
`-- java
`-- com
`-- mycompany
`-- app
`-- AppTest.java
构建配置文件内容如下:
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
</project>
这两点在Maven中是POM(Project Object Model)的内容,POM即项目对象模型,是Maven2引入的概念(Maven1已经不用了,不管他了),Maven会为每个项目,根据其构建配置文件,建立一个模型,然后根据这个模型来构建项目;第三,Maven引入了中心库依赖管理,即开发者可以将自己的库Jar包上传到Maven中心仓库(这个仓库自己也可以搭建,也可以使用Maven官方的免费仓库),其它开发者在pom.xml中申明该依赖(填写地址和版本号),构建的时候,Maven会自动从中心仓库下载,还可以解析依赖链,下载所有对应的库文件,例如:
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.mycompany.app</groupId>
<artifactId>my-app</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
正如官方所述,Maven的目的是:1)简化项目构建;2)建立一个标准的统一的项目构建方法;3)对项目的组成有一个清晰的描述;4)简化的项目发布,不同项目之间共享jar,即更好支持多项目;5)简化Java开发者的日常工作。在我看来,Maven是构建工具史上的一次大的重构,敢于标新立异,打破常规,重新思考并从头设计。Maven推出后,由于之前积累了太多优秀的Ant构建的项目,所以Apache给Ant加了一对翅膀——Ivy,帮Ant实现中心仓库依赖管理,Ivy跟Ant的风格一致,相比Maven,有更加十分灵活的配置。
Gradle介绍:
2004年,Maven发布后(实际上2002就已作为Apache Turbine的子工程存在),非常受Java开发人员的爱戴。然而,软件史上,再伟大的项目都有他的丧失光芒的那一天,不过旧项目的过时往往是另一个更强大的新项目的诞生,Gradle就是Maven的后生来者,在他的问世之初就受到各大开源社区和业界的好评,在短时间内就收到了极大的关注,也在那时,Google迅速将Android的编译环境迁移到了Gradle,当你搜索Gradle时,你应该会有这样的感觉,为何人们都如此爱好这个工具,其中很大一部分原因是Gradle使用了基于Groovy的DSL语言作为构建脚本语言的而不是以往的XML,这里应该会迅速产生两个疑问:1.Groovy是什么;2.DSL是什么。
首次Groovy是一种编程语言,在我看来,Groovy是能运行在Java虚拟机上的“Python”,当然他不是Python,况且也存在真正能够运行在JVM上的JPython,那我为什么称之为运行在JVM上的Python呢,在《Groovy In Action》的前言中提到,Groovy的作者非常喜欢Python,但出于一些限制,所以他创造了一种能够在JVM运行的类似的语言,在Groovy里,极大的借鉴了Python里的基本数据结构及语法,使得很简洁的代码就可以在Java虚拟机里运行,大家都知道解析型语言的特点就是语法简洁,很短的代码可以做很多事,而且免编译,调试非常方便,还可以交互式编程,像敲命令一样,很多时候加入解析型语言混合编程可以极大的提高效率,例如使用解析型语言写单元测试非常快,写小助手或小工具也十常快,在Java项目中,使用Java和Groovy混合编程是很简单的事,他们可以相互调用,所以我很想将这种混合编程的方式引入Android开发中,以便使用Groovy写单元测试以及用Groovy作为插件代码,在线下载运行,都很方便,不过我至今还在实验阶段。
DSL就简单了,DSL即Domain Special Language,领域专用语言,别管他名字这么高深,其它含义很简单,领域专用即只用于某个领域的语言,与之对应的是通用语言,例如,Java和C就是通用语言,而Makefile构建脚本里的就是领域专用语言,这种语言有简单的语法,但只能用于构建项目,没人能用Makefile语言开发一个游戏吧。那什么是基于Groovy的DSL语言,这要说Groovy的别一个伟大的特性了,即对领域语言的强大支持性,之所以有这么强大的支持性,又一部分原因是因为Groovy内建强大的操作元数据(Meta-data)的能力(这也是Groovy优势,且是Java的短板),元数据又得解释一下,简单说元数据就是一些内在属性,比如你有一个对象,那这个对象的类,这个对象的创建时间,就是他的元数据,又比如一个类,这个类的成员列表,方法列表就是这个类的元数据,一般情况下我们不会用到这些元数据,但想在这个语言基础构建另一门DSL语言就必须访问和操作元数据,Groovy构建DSL语言的原理大概如下,你可以注册一个监听器,当Groovy代码运行的时候,有方法调用的时候会通知你,有参数传入的时候会通知你,创建对象的时候会通知你,有点像AOP,你收到这些通知可以做什么多事,比如你随便写一段代码放到Groovy文件里,是不遵循Groovy语法的,这时,Groovy系统就会通知你有一段这样的代码来了,而你可以根据这段代码做任何事,比如你收到一个@号,就做勾股定律运算(即对x平方+y平方的和求平方根),收到一个#号就求圆周长运算,然后把执行结果返回给Groovy系统,这样人家写一个int a = 3@4; // a 将等于5 这样代码就是你刚刚创造的DSL语言,你可以运行起来,还可以给他取名为NiuBi语言。
这样解释,大家应该明白Gradle为什么这么强大了吧,别人用的是XML文件,而Gradle用是编程语言,虽然Gradle里是用了基于Groovy的DSL语言,但符合Groovy语法的语句大多可以在Gradle脚本中直接运行,这样相当于Gradle脚本有了通用语言的功能,这样你有个性化定制需求的时候,就可以使用你平时编程同样的思路去实现,而DSL的特性又简化了常用构建需求的实现,于是即有灵活和强大的扩展性,又有易用性,其实是结合了通用语言和DSL语言的优点。其实这也是软件发展的必然结果,随着软件的发展,软件复杂度肯定是越来越复杂,人们对构建的需求肯定会越来越多,以至于今天的构建需求的复杂度达到了之前普通程序的复杂度,以至于构建需求也需要通用语言才能满足,而DSL语言只为了简化常用需求的实现,增加便捷性。
Gradle即灵活又易用还有其它原因,一个重要的原因是,Gradle里引入了插件的思想,对于常用的构建需求都通过了插件来简化,比如Java插件,当你应用它时,构建Java的时候就跟Maven一样简单快捷,这个思想很巧妙,Gradle脚本具备通用语言的灵活性,同时将那些常用的构建任务封装成插件,用于简化常用任务,使得Gradle即强大又快捷。我们还可以自己写插件,虽然Maven也有类似的扩展,但没有Gradle方便,原因还在Groovy,因为可以用Groovy语言写插件,代码可以很简短,同时不用编译,Maven的扩展是用Java写了之后编译的。
此外,在依赖管理方面,Gradle最先使用了Ivy,后来自己开发了一个全新的依赖管理模块,跟Ivy不同,能同时对接Maven和Ivy的中心仓库。Gradle的Java插件也约定了和Maven一样类似的目录结构。
下面介绍一下Gradle脚本的几个概念,Gradle两个核心概念:Project和Task,这与Ant类似。每个脚本包含一个或多个Project,每个Project由一个或多个Task,一个Task包含一个或多个Action,一个Action就是一个代码块,代码块就是一些跟通用语言一样的语句,Gradle还可以使用Ant Task。一个简单的Gradle脚本如下:
task hello {
doLast {
println 'Hello world!'
}
}
可以看出Gradle就是在做通用语言做的事, 一个编译Java项目的Gradle如下:
apply plugin: 'java'
没错,只有一行,和Maven一样,约定了类似的目录结构,将会编译
src/main/java下的源代码。
使用Gradle构建Android:
Android使用Gradle做为构建工具,其实只是Google做了一个Gradle Android Plugin,在Gradle脚本中应用Android Plugin之后,就可以很方便的构建Android项目了,本文内容只对构建结构介绍,希望大家能看懂脚本,并能添加简单的功能,并不包含step by step教程,因为这种类型的好文章太多了,再多写也没意义,一个简单的例子如下:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.0.1'
}
}
apply plugin: 'com.android.library'
android {
compileSdkVersion 21
buildToolsVersion '21.1.1'
}
dependencies {
compile fileTree(dir: 'libs', include: '*.jar')
}
也许和你以前看到的例子不同,这个是精简版的,比Studio生成的例子还简单一点,这一个文件(除了local.properties)就可以编译一个项目了。
不过,通常情况下,一个项目的结构稍复杂点,一般项目根目录有一个build.gradle和settings.gradle,而根目录下包括若干个模块,每个模块下都有一个build.gradle。如:
HelloAndroidGradle
|-- build.gradle
|-- settings.gradle
|-- local.properties
`-- app
|-- build.gradle
`-- libA
|-- build.gradle
local.properties的作用很简单,用于存一些本地配置,如Android SDK的路径,settings.gradle主要是用于指明包含哪些模块,如:
include 'app'
include 'libA'
如果你发现很多地方有settings.gradle,不用管它,因为只有项目根目录的settings.gradle才会生效,当然你也可以使用子目录作为项目的根目录,如app目录,这样可以从app目录构建项目;根目录的build.gradle是一些全局的东西,一般包含一个buildscript的代码块,是用于是配置构建工具的,比如构建脚本自身依赖的Android Plugin,如:
buildscript {
repositories { // 编译脚本的仓库配置,用于搜索脚本本身的依赖库
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.0.1' // Android Plugin
}
}
allprojects { // 全局仓库配置,用搜索项目的依赖库
repositories {
jcenter()
}
}
你的项目本身是不依赖Android Plugin的,这里的依赖库是不会打包到APK中的;app和libA都是一个模块,其下的build.gradle用于构建本模块的。这里的一个模块相同于Eclipse中的一个工程,如果在Eclipse里,app就会为主工程,libA为库工程,app依赖libA。app和libA的build.gradle内容分别如下:
apply plugin: 'com.android.application'
android {
compileSdkVersion 21
buildToolsVersion '21.1.1'
}
dependencies {
compile fileTree(dir: 'libs', include: '*.jar')
compile project(':libA')
}
与
apply plugin: 'com.android.library'
android {
compileSdkVersion 21
buildToolsVersion '21.1.1'
}
dependencies {
compile fileTree(dir: 'libs', include: '*.jar')
}
只有一行不同,dependencies语句块配置了依赖关系,其中fileTree(dir: 'libs', include: '*.jar')是指libs目录下的所有jar文件,compile project(':libA')是指依赖另一个模块libA,当然还可以申明存在于中心仓库的依赖,如:
compile 'com.android.support:appcompat-v7:21.0.3'
如果你使用Gradle构建项目,从命令行构建与使用Studio构建是一样的逻辑,Studio会根据同步build脚本建立IDEA内部的配置,当你修改build脚本时,Studio也会提示你同步,所以建议大家使用build脚本来配置构建需求,而不是使用IDE,以免IDE会调整你的build脚本导致不易读。
扩展:
能在Gradle脚本中配置的项太多,本文不打算一一例举,请大家参考Gradle Plugin User Guide,其中有几个Android Plugin新增的概念不太好理解,我在此做一下解释,未见过下面东西的同学无视之。
1)Build Type:构建类型,包括release和debug;
2)Product Flavor:产品风味(不好翻译),用于创建不同特性的产出物,如免费版和付费版;
3)Build Variant:构建变种(中文翻译真难听),Build Type + Product Flavor = Build Variant,以上两个元素不同的组合就产出不同的变种,如免费版的debug版。
4)Flavor Dimensions:风味维度,用于创建出复合产品风味,这种产品风味是由多个风味维度组合出来的,例如:一个项目的发布的版本一方面可以从处理器架构来分为arm、x86,mips,另一方面又可以分为免费版和付费版,所以最终的产品风味肯定是这两个维度的组合,如arm免费版、arm付费版、x86免费版、x86付费版、mips免费版,mips收费版。当你的产品风味很多的时候,比如大于3个维度,每个维度的取值还很多,就可以使用这种复合产品风格,来简化build脚本。注意,使用风味维度时,写法有点奇怪,是用逆向思维,申明维度之后,先写出维度的取值,再写出这个取值属于哪个维度,如:
android {
...
flavorDimensions "abi", "version" // 申明有两个维度:abi和version
productFlavors {
freeapp { // 维度的取值
flavorDimension "version" // 这个取值属于名为version的维度
...
}
x86 { //维度的取值
flavorDimension "abi" // 这个取值属于名为abi的维度
...
}
...
}
}
结束语:
参考资料:
· Java Build Tools: Ant vs Maven vs Gradle
· 《Groovy In Action》
· DSL Wiki