前言
从2013年Google推出Android Studio(后面以AS简称)开始,到现在已经历经3年,版本也发展到了2.x版本,目前Android开发者基本上已经没有再用Eclipse开发的了。从Eclipse切换到AS,项目组织结构和环境变动很大,不过上手AS还是很简单的,新建一个项目,选择API版本,选择一个默认的空Activity,然后点击确定一个项目就创建好了,直接点击运行就可以把项目运行到你手机上或者模拟器上面。
上面很容易创建了一个Android项目,然后就可以愉快的进行开发了。很快遇到了一个需要显示网络图片的需求,先看看网上有没有现成的轮子,随便一搜发现Facebook出品的一个叫Fresco的库,网上一片好评,那就用它了。安装官方说明,找到项目build.gradle文件,在dependencies段加入如下一行
compile 'com.facebook.fresco:fresco:0.14.1'
同步一下项目,然后在代码中发现已经可以引用到fresco库了,虽然不懂是怎么解决的,但是既然已经能用了 ,就继续开发。很快项目开发好了,准备发版测试了,突然QA提出要记录每次打包的时间,显示在应用内的调试页面,这样以免以后撕逼时乌龙,果然是老司机!怎么实现呢?官方文档翻了几遍,终于看到一篇讲怎么注入变量到Manifest里面的:
android {
defaultConfig {
manifestPlaceholders = [hostName:"www.example.com"]
} ...
}
思路有了,我在AndroidManifest里面配置一个变量表示打包时间BUILD_TIME,然后打包时利用上面的方法把当前时间赋值给BUILD_TIME,程序从AndroidManifest中读取BUILD_TIME显示到调试面板中,经历各种Google细节完成如下:
<!--AndroidManifest中配置打包时间->
<meta-data android:name="BUILD_TIME" android:value="${BUILD_TIME}" />
def releaseTime() {
return new Date().format("yyyyMMdd hh:mm:ss", TimeZone.getTimeZone("GMT+8"))
}
defaultConfig {
multiDexEnabled true
applicationId "xxxxx"
minSdkVersion 16
targetSdkVersion 23
versionCode 3
versionName "1.1.1"
manifestPlaceholders = [BUILD_TIME:"${releaseTime()}"]
}
程序运行下看看,竟然好使!等等,defaultConfig里面那堆东西看起来那么面熟啊,这不是AndroidManifest里面配置的东西么,这里面到底还能塞进来什么东西?我咋知道minSdkVersion有没有拼写错误啊?不管了,反正程序现在能跑,赶紧继续发版,今天还要早点下班约会,虽然不知道跟谁约。
刚打好包运营妹子就跑了过来,说哥哥我要发四十个渠道,每个渠道都要统计用户量,你就扔给我一个包,真的好吗?额,这个等一下,马上处理。统计渠道好搞,应用中都集成了友盟,只需要在下面的配置里写上渠道名,打个包,再修改渠道号打包,重复四十次就能搞定了。
<meta-data android:name="UMENG_CHANNEL" android:value="360" />
当然我不会这么傻手动打包四十次,好歹也是计算机科班出身的码农,虽然我确实不知道该怎么办,但是我可以问百度啊(这事可以问百度,渠道和友盟都是国情嘛)。很快找到解决方案:
productFlavors
{
360{}
yingyongbao{}
wandoujia{}
xiaomi{}
......
}
productFlavors.all { flavor ->
flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE:name]
}
buildTypes {
debug {
......
}
release {
buildConfigField "boolean",
minifyEnabled true
zipAlignEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
applicationVariants.all { variant ->
variant.outputs.each { output ->
def outputFile = output.outputFile
if (outputFile != null && outputFile.name.endsWith('.apk')) {
//MyApp_xiaomi_2016-10-18_v6.2.0.apk
def fileName = "MyApp_${variant.productFlavors[0].name}_${releaseTime()}_v${defaultConfig.versionName}.apk"
output.outputFile = new File(outputFile.parent, fileName)
}
}
}
}
}
果不其然,问题完美解决,但是现在真的没办法淡定的继续写开发了,这里一坨一坨的东西不把它弄清楚,相信后面的日子会很难过,那么我们就开始研究一下这些东西吧。
1. Gradle For Android
Google在推出AS的时候就说了采用Gradle替代Ant来构建项目,不难猜出,上面我们写的就是Gradle“代码”了,那我们再来看一下Gradle到底是什么东西。很快我们找到了它的官方用户手册---https://docs.gradle.org/current/userguide/userguide.html, 里面它是这样自我介绍的:“a build system that we think is a quantum leap for build technology in the Java (JVM) world”,它谦虚的表示自己是Java世界中编译技术的一项巨大突破的一个编译系统,使用了Groovy作为编译脚本,定义了大量的领域模型(domain model)。嗯,还不是很懂它在讲什么,那么我们先把文档看一遍吧。。。纳尼,竟然72节,有点看不下去,并且看标题很多跟Android没关系,还是先学会怎么写点简单的Android配置吧。还好Android官方文档里面有一章介绍怎么用Gradle配置工程(https://developer.android.com/studio/build/index.html), Gradle官网上也有也有一个模块专门介绍Gradle在Android中的应用(https://gradle.org/getting-started-android-build/#build-master), 并且提供了一本电子书和一个讲解视频,那就赶紧先上车吧。
1.1 Gradle Wrapper
当构建一个项目时,往往要需要对应的工具支持,如果本地没有安装或者本地安装的版本和项目要求的不一致时,就会比较麻烦。Gradle引入了Gradle Wrapper很好的解决了这个问题。我们先看一下Gradle Wrapper的组成部分:
- gradlew (Unix Shell script)
- gradlew.bat (Windows batch file)
- gradle/wrapper/gradle-wrapper.jar(Wrapper JAR)
- gradle/wrapper/gradle-wrapper.properties(Wrapper properties)
看起来是不是很眼熟,去看一下上面创建的Android项目,可以看到项目根目录就有这几个文件,gradlew和gradlew.bat分别是Unix系和Windows系操作系统下的命令,打开gradle-wrapper.properties,可以看到有下面的一行配置:
distributionUrl=https\://services.gradle.org/distributions/gradle-2.14-all.zip
这个即本项目需要的gradle版本号。平时执行Gradle命令任务时,使用的是
gradle <task>
但是以后你可以使用(Mac上,windows上类似) :
./gradlew <task>
它会先看当前是否已经安装gradle并且当前指定的gradle版本本地已经存在,如果不是的话,它就先自动下载对应的gradle版本然后在执行task。如果想改变当前项目对应的gradle版本,可以直接编辑gradle-wrapper.properties文件或者在AS中打开module setting,选择"Project"选项,在右边可以直接编辑。也可以使用下面两种方式:
gradle wrapper --gradle-version 2.14
task wrapper(type: Wrapper) {
gradleVersion = ' 2.14 '
}
1.2 Gradle 项目结构
Gradle所有的工作就是基于projects和tasks,我们可以先暂时不用理解什么是projects和tasks。每一个Gradle构建都包含一个或多个project,而每一个project就是一个待编译的工程,它通常包含一系列的task。Gradle要求每一个project在它根目录下都要有一个build.gradle,官方称一个build.gradle文件为"编译脚本",它定义了一个project和它的tasks。在继续之前,我们可以先写个小例子熟悉一下,创建一个文件build.gradle,打开并且输入一下代码:
task taskX(dependsOn: 'taskY') << {
println 'taskX'
}
task taskY << {
println 'taskY'
}
在同级目录输入如下命令看下输出
gradle taskX
上面就是一个简易的project。如果一个项目里面有多个project怎么办?平时开发时经常会把一个项目拆分成多个子项目。这种情况Gradle中称作Multi-Projects Build,一个Multi-Projects Build由一个根project和一个或多个子project组成,这些子project也 可以包含自己的子project,它的组织结构如下:
root/
build.gradle
settings.gradle
subpro1/
build.gradle
subpro2/
build.gradle
我们可以看到每个project都要对应一个build.gradle,这个我们上面已经讲过了,但是这里在根目录多出来一个settings.gradle。这个就是Multi-Projects Build的奥妙所在,这个settings.gradle主要用来配置这个Multi-Projects Build包含哪些子project。如果你有多子工程的Android项目,打开看看gradle的结构是不是跟上面一样。当然,如果你不想把subpro1和subpro2放到root目录下也没关系,只需要在setting.gradle中指定子project的目录即可,参考目录结构和setting配置:
project/
root/
build.gradle
settings.gradle
app/
build.gradle
subpro1/
build.gradle
subpro2
build.gradle
//setting.gradle
include ':app', ':subpro1', ':subpro2'
def projectTreeRootDir = new File("../");
project(":subpro1").projectDir = new File(projectTreeRootDir, "subpro1");
project(":subpro2").projectDir = new File(projectTreeRootDir, "subpro2");
如果你重来没有使用过Multi-Projects Build项目,建议你尝试一下怎么为当前项目添加一个子Module,对的,在Android Studio中每个子项目叫做Module,以后你项目大了,肯定有必要把它拆成多个Module。创建过程很简单,在Project视图下,在当前项目根目录上面右键,"new"->"Module"->"Android Library" (或者"Phone & Tablat Module"),这里就不贴图了。
1.3 Gradle Recipes For Android
前戏了这么久,该进入正文了,我们打开自己的Android项目,挨个研究一下里面的Gradle配置吧。
settings.gradle:
它位于根目录下,用来配置构建一个应用时都需要编译那些module,所以你项目中的所以子project都要在这里配置,通常如下:
include ':app', ':submodule1', ':submodule2'
Top-level build.gradle:
它位于根目录下,用来配置此项目下所有module都应用的设置。下面是常见的一个例子以及说明。
/**
buildscript{}块配置Gradle自己的仓库(repositories)和依赖(dependencies),
如下面所示,Gradle如果想要能编译Android项目,需要依赖谷歌提供的一个gradle插件
(Gradle Plugin),它里面包含了一些编译需要的指令。
*/
buildscript {
/**
repositories{}配置了Gradle查找和下载依赖的仓库,通常它默认配置了JCenter、
Maven Central 和 Ivy等远程仓库,你也可以配置自己的本地仓库和私人远程仓库。
*/
repositories {
jcenter()
}
/**
dependencies {} 配置了Gradle编译项目需要的依赖,这里制定了依赖2.0.0版本
的Gradle Plugin
*/
dependencies {
classpath 'com.android.tools.build:gradle:2.0.0'
}
}
/**
allprojects {}配置你项目中所有module都使用的仓库(repositories)和依赖
(dependencies)。*/
allprojects {
repositories {
jcenter()
}
}
Module-level build.gradle
也就是每个子工程的build.gradle,通常位于<project>/<module>/目录下面,主要设定当前module的配置信息。同样,我们也根据下面的例子解释:
/**
在当前工程中为Gradle使用Android插件,可以支持使用android{}配置Android专有的
编译选项,插件为 com.android.application 表示可运行Android项目,如果为一个aar库
时应该应用com.android.library */
apply plugin: 'com.android.application'
/**android{} 配置所有Android编译相关的选项*/
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
/**
默认的编译变量和设置,可以动态的覆盖AndroidManifest.xml里面的属性
*/
defaultConfig {
applicationId 'com.example.myapp'
minSdkVersion 14
targetSdkVersion 23
versionCode 1
versionName "1.0"
}
/**
编译类型,默认有debug和release,主要设置打包、混淆相关参数。
*/
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
/**
产品配置,可以设置多个产品选项,覆盖defaultConfig{}里面的值。
直观点讲就是可以一下打多个包,每个包有不同的设置。
*/
productFlavors {
free {
applicationId 'com.example.myapp.free'
}
paid {
applicationId 'com.example.myapp.paid'
}
}
/**
splits{}可以配置编译不同的APK,每个包只包含针对特定屏幕密度或ABI的代码和资源
*/
splits {
density {
enable false
exclude "ldpi", "tvdpi", "xxxhdpi", "400dpi", "560dpi"
}
}
}
上面写了貌似挺详细了,但有感觉啥用没有,很多东西还是一知半解,前面提到的问题----这里面到底都支持什么参数?还是没有解决。还好我找到了Android Gradle DSL 我们选择左边的“AppExtension”,右边的“ Properties”可以看到android{}块支持的所以配置字段和解释,我们可以沿着这个根找到所以其他相关字段的配置,例如buildTypes里面的每种type支持哪些字段,查找到的截图如下:
另外,我们发现左边还有一个“LibraryExtension”,估计不说你也能猜到了,它和前面提到的“AppExtension”分别对应com.android.application和com.android.library。属性值太多了,这里就不挨个解(翻)释(译)了。
Gradle Properties Files
这里主要指根目录下gradle.properties和local.properties两个文件。前者主要是指项目范围内的Gradle配置,例如Gradle进程堆栈大小。后者主要配置一些本地SDK路径,一般IDE会自动生成,所以最好不要编辑和提交到版本控制系统里(svn, git)。
2. In-Depth Study Of Gradle
看完上面的东西,应该已经可以翻着文档配置下Android构建脚本了,但是想要在构建时配置写高级的东东,发现前面的知识还是不够用,那我们接下来就深入点看看Gradle的知识吧。
2.1 Task
Task是指编译脚本中的一个独立的编译单元,代表了一些要执行的动作和行为,它是project的组成元素,可以包含多个action(block), 在上面的例子中我们已经演示过怎么写一个task, task有两个函数doFirst和doLast,表示最先执行和最后执行的action。下面我们写一个例子
task mytask
task task2 {
println "test2"
}
task task3 << {
println "task3"
}
mytask.doFirst {
println "do first"
}
mytask.doLast {
println "do last"
}
task task2 {...}这种写法是表示创建task2后返回前,先执行block的内容。而 task task3 << {...}是doLast的一种简写形式。通过命令 "gradle taskName"可以执行一个task,如下命令可以看到当前所以的task:
gradle tasks
gradle tasks --all
而我们之前的例子中可以看到tasks之前是可以存在依赖关系的,所以整个项目构建就可以通过这些tasks来完成了。
2.2 Build Lifecycle
构建主要分三个阶段:初始化、配置和执行。在初始化阶段,主要确定哪些project参与构建,并且创建一个Project对象。配置阶段,主要是解析每一个project的脚本文件,确定所有的task并且根据他们的依赖关系创建一个有向图,最后一个阶段就是执行这些tasks。而我们在这些阶段之间,可以插入一些hook来完成一些特殊的需求。下面是几个例子:
preBuild.dependsOn 'dfqin'
task dfqin << {
println "do after dfqin task"
}
tasks.getByName("preBuild"){
it.doLast {
println "$project.name: after preBuild"
}
it.doFirst {
println "$project.name: before preBuild"
}
}
project.afterEvaluate {
println "after evaluate"
}
2.3 Gradle Build Language(DSL)
Gradle 是一种可配置脚本,主要体现在它可以定义一些SB( script blocks),SB类似于一个函数调用,block作为参数传递给调用者来完成特定的配置工作,即它可以称作“领域描述语言”。我们可以看下官方文档
上面的截图中的解释看懂了吧?我就不解释了———你如果看懂了,给我解释一下,因为我没看懂:( 好了,不懂也得死磕,它讲到,Gradle是可配置脚本,当脚本执行后,它会配置(生成)一个特定类型的对象。脚本也分类型,有"Build script"类型的脚本,有"Init script"类型的脚本,有"Settings script"脚本,执行不同类型的脚本会生成不同的对象,如上三种类型脚本分别生成Project、Gradle和Settings对象。下面讲到常用的一些Build script可以由一些语句(方法调用、属性赋值和变量定义)和SB(script block)构成,我们在上面的截图中看到了一些熟悉的东西,就是上面所说的SB,以buildscript{}为例,我们去项目中看一下,发现竟然可以按command进入到源码,截图如下:
我们可以看到,它是Project接口的一个方法,现在我可以这样理解,我们要创建一个Project类型的对象,这个对象的方法实现就是依靠我们的SB,系统后面调用这个对象的方法时,就是执行了我们的SB,通过这种方式,我们实现了对构建项目的配置。嗯,现在不管你懂不懂,反正我是懂了,以后再看到如下的脚本,我就知道它是一个SB,是可以执行的一个方法,大括号中间的东西是一个block,作为参数传递给方法的。关于这个block,我们可以叫做闭包,js、swift中都有,我们这里是groovy的闭包,三言两语解释不清,我也没能力解释清楚:`( 就理解成它是一段可以执行的代码好了。这样看,它的确比那些靠配置xml构建的工具强大一些(也难学一些)。
buildscript {
repositories {
maven {
url uri('./repo')
}
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.1'
}
}
那如何知道buildscript这个SB里面会有repositories和dependencies这两个SB呢?我们可以看下文档,buildscript这个SB是回调给ScriptHandler对象的,而这个对象中我们可以找到对应的两个方法。以后再“写”脚本时,网上没有可以参考(抄)的案例时,我们就可以这样翻着文档来实现了。
2.4 Gradle Plugin
这两年热更新比较火爆,各厂和一些大牛纷纷开源了自己的热更新方案,去研究这些开源方案,发现大家几乎都定制了自己的Gradle插件,项目在使用它们时一句“apply xxx”风轻云淡的就搞定了。那到底什么是Gradle插件呢?我们在官网看他们讲到,Gradle的核心功能提供的东西其实很少,各种有用的特征例如编译Java代码都是由插件完成的。插件可以添加新的task、配置等,也能扩展其他的插件,下面我们尝试使用Android Studio写一个插件:
- 1、 新建项目。新建一个Android项目,里面默认会有一个名称为app的module,我们会在次项目里面创建一个插件,并在app里面引用我们创建的插件。
- 2、创建插件module。在工程中新建一个子module, 类型为Android Library,这里的module名称就是插件项目名,这里我们命名为myplugin。
- 3、初始化插件module项目结构。因为AS默认没有groovy项目模板的,我们需要手动构建项目结构,把myplugin项目内的东西全删了,只留build.gradle,此文件内容也清空。然后创建目录src/main/groovy,groovy会被识别成groovy源码目录,然后我们在创建src/main/resources/META-INF/gradle-plugins目录。目前myplugin已经被我们改造成gradle插件项目结构了。
- 4、创建插件实现文件,这里我们创建一个名字为MyPlugin.groovy的文件并且放到对应的包名目录中。下面代码中我们创建了一个名为"printTask"的task,项目中如果引用了这个插件,就能调用此task,这里是groovy语言实现的 ,这样的话能做的事就比较多了。
package com.dfqin.plugin
import org.gradle.api.Plugin
import org.gradle.api.Project
public class MyPlugin implements Plugin<Project> {
void apply(Project project) {
project.task('printTask') << {
println "this is a plugin task"
}
}
}
- 5、创建属性文件。我们在src/main/resources/META-INF/gradle-plugins目录下创建dfqin.plugin.properties文件,文件名 就是对外发布的插件名,在其他项目中使用此插件时需要声明如下:
apply plugin: 'dfqin.plugin'
打开dfqin.plugin.properties文件,输入如下配置:
implementation-class=com.dfqin.plugin.MyPlugin
这里我们把插件指向了我们上面groovy实现的类,使用插件时就能找到这个类了。
- 6、配置插件module的gradle文件。打开myplugin下面的build.gradle,输入以下代码:
apply plugin: 'groovy'
apply plugin: 'maven'
dependencies {
compile gradleApi()
compile localGroovy()
}
repositories {
mavenCentral()
}
///********** 分割线 ************///
group='com.mygroup'
version='1.0.1'
uploadArchives {
repositories {
mavenDeployer {
repository(url: uri('../repo'))
}
}
}
这里用斜线做了一个分割,上面的部分是插件的gradle配置文件,用来编译生成插件时使用。而分割线下面的是配置发布插件到maven仓库的。这里我们发布到当前工程的目录里。这里的group和version对应maven坐标的groupId和version,而modulename即我们这里的myplugin对于坐标的artifactId。按照上面的配置,我们可以通过com.mygroup:myplugin:1.0.1来找到我们的插件。下面我们看一下现在的项目结构图:
- 7、发布插件到本地仓库。我们只需要运行我们前面写的task uploadArchives即可。在命令行下面运行下面命令或者直接在AS的gradle视图里面找到此task双击运行。等执行结束,在项目根目录应该可以看到一个repo的目录,我们在里面可以找到生成的plugin插件。
./gradlew task uploadArchives
- 8、使用插件。这个就跟平时使用插件一样了,首先配置maven仓库地址,我们默认都配置了jcenter,这里因为我们的插件在本地仓库,所以要加上一个本地仓库,然后在项目的gradle文件中使用插件。项目根目录的build.gradle文件中配置如下:
buildscript {
repositories {
maven {
url uri('./repo')
}
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.2.1'
classpath 'com.mygroup:myplugin:1.0.1'
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
再在app module的build.gradle中添加
apply plugin: 'dfqin.plugin'
这时候我们就可以运行我们再插件中定义的printTask了,一个最简单的插件即完成了。运行插件中的task结果如下
注意:因为目前的插件和使用插件的module app在一个项目中,所以当你编辑脚本执行uploadArchives有时会报错,但真正的错误并不一定是插件的错误,有可能是app module中的错误,而生成插件时所有的gradle脚本都会执行。demo已放到github上面,地址:https://github.com/dfqin/GradleDemo
3. 后记
最早是因为要在小组里面分享gradle,想写篇文章整理下知识点吧,后面发现需要讲的内容比较多,加上有时项目忙,所以文章写得断断续续,整片文章连贯性也并不好。目前gradle相关的知识点基本覆盖掉了,只是比较粗略,从宏观上介绍了整体轮廓,没有多少能直接拿到项目中使用的东西,但是根据上面内容应该可以找到解决问题的方法。
部分参考文章:
http://blog.csdn.net/sbsujjbcy/article/details/50782830
http://blog.csdn.net/innost/article/details/48228651#comments
https://segmentfault.com/a/1190000004229002