Gradle是Android Studio默认的构建工具,如果是基本的APP开发,不会涉及到Gradle太多内容,毕竟它的诞生就不是专为Android服务的。
日常开发需要涉及到使用Gradle的场景相当有限,比较频繁的就是对应库,如jar,.so文件的导入,如果应用本身方法数比较多,尤其是导入太多第三方库就容易出现这个问题,就需要用到MultiDex的相关内容,如果需要在编译的时候区分debug和release等版本,是否混淆或者自动打包等,这些都会涉及到Gradle的编写,但网上都有现成的例子,直接拿来用就可以一直保持Gradle文件在应用版本迭代中基本保持不变。
所以这里有一个关键的地方:如果要学习Gradle,界限在哪里。
从Android开发人员角度来看,这是一个相当重要的问题,毕竟Gradle也是Android开发中相当重要的部分,如果对这块不熟悉的话,就有种表面上走在康庄大道,但实际上却是被固定住头部不能往下看脚底踩的到底是不是正常的路的感觉。
首先,我们在创建Android应用程序的时候,Android Studio默认的Android结构已经显示得很清楚的了:
我们来看看Gradle Scripts的相关文件。
看一下第一个build.gradle文件。
它后面的说明表明这是Project的build.gradle文件,也就是根目录的build.gradle文件,然后我们看一下文件的内容:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.0.0'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
buildScript在Groovy(Gradle的DSL,domain specific language,领域专门语言)中是一种method的调用,传入的参数为configuration closure,执行后会对Project的属性进行配置。
Closure,闭包,对于程序员来说,是一个相当熟悉的概念,不同语言都有闭包的实现和它们各自的意义,而Groovy是基于JVM的语言,它的闭包和Java是一致的,相当于一个匿名函数。
在Groovy中,我们可以这样写一个闭包:
{ a, b -> a + b }
如果参数只有一个,我们可以用it来替代:
{ it -> print it}
甚至可以连这个it都可以省略,直接写成:
{ print it }
因此,我们可以理解上面这个闭包为什么会这样写。
repositories表示代码仓库的下载来源,这里的来源是jcenter。
Gradle支持的代码仓库有几种类型:
(1)Maven中央仓库,不支持https访问,声明方法为mavenCentral()
(2)JCenter中央仓库,实际上也是用Maven搭建,通过CDN分发,并且支持https访问,也就是我们上面默认的声明方法:jcenter,如果我们想要切换成http访问,就要修改配置:
repositories { jcenter { url "http://jcenter.bintray.com" } }
(3)Maven本地仓库,可以通过本地配置文件配置,通过USER_HOME/.m2/
下的settings.xml配置文件修改默认路径位置,声明方法为mavenLocal()
(4)常规的第三方maven库,需要设置访问url,声明方法为maven,这个一般是有自己的maven私服
(5)Ivy仓库,可以是本地仓库,也可以是远程仓库
(6)直接使用本地文件夹作为仓库,声明如下:
repositories { flatDir { dirs 'lib' } flatDir { dirs 'lib1', 'lib2' } }
Gradle会优先使用服务器仓库,如果没有才会去找本地仓库。
一般的项目需求采用的第三方基本都会提交到JCenter,所以大部分情况下直接使用默认的就行,考虑到国内访问速度,可以提前下载好对应的库,然后放到自己或者公司的maven私服上,再指定对应的maven地址。
dependencies表明项目依赖对应版本的Gradle构建工具,但更加具体的版本信息却是在gradle-wrapper.properties这个文件中,具体如:
#Mon Dec 28 10:00:20 PST 2015 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https://services.gradle.org/distributions/gradle-2.10-all.zip
我们下载下来的是gradle-2-10。
allprojects指定所有参与构建的项目使用的仓库的来源。
这里我们有一个疑问:buildscript和allprojects都指定使用的仓库的来源,它们的真正区别在哪里呢?
实际上,allprojects是用于多项目构建,在Android中,使用多项目构建,其实就是多Module构建。
我们看settings.gradle这个文件:
include ':app'
通过include将app这个module添加进来。
从根目录开始,一直到include进来的所有module,都会执行allprojects的内容。
我们也可以通过subprojects来指定module和根目录不同的行为。
我们是否可以指定某个module执行它特有的行为呢?
当然可以,通过project(':moduleName'){}就能指定该module自己的行为。
在Android中,多项目的构建是很正常的,因为我们添加的library也是一个module,不过一般而言,子项目和根项目的配置大部分情况下都是一样的,大部分操作都是在app module中,而其他library module只是提供API而已。
buildscript主要是为了Gradle脚本自身的执行,获取脚本依赖插件,在Android中,我们的构建脚本就是Gradle。
repositories也可以是根级别的,为当前项目提供所需的依赖包,但在Android中,这个跟allprojects的repositories的作用是一样的。
同样dependencies也可以是根级别的。
我们在Android Studio 2.0正式版本中生成应用的根目录的build.gradle文件中,结尾还看到这样的代码:
task clean(type: Delete) {
delete rootProject.buildDir
}
这里声明了一个clean的task,它会在我们执行gradle clean时,删除根目录的build目录。
看一下这个面板:
可以看到各种task,我们的应用之所以能够编译,运行和安装,都是这些task在发挥作用。
我们再看一下proguard-rules.pro这个文件,这个是混淆文件,在这里添加项目的混淆规则。
gradle.properties可以设置gradle唤起的daemon进程,比如JVM参数,如果Gradle编译速度和网速没有关系,那么有可能是Gradle的daemon进程的JVM参数太小了,因此可以在这里进行配置。
local.properties是指定SDK的存放地址,在Android开发中,.gitignore文件默认就有忽略这个文件的设置,因为每个人的SDK位置都有可能是不同的。
Android Studio提供了多种工程目录结构,其中最常见的是Android和Project,Project和Eclipse是一样的,而.gitignore也只有在Project工程结构下才能看到。
根目录的builde.gradle文件我们一般都不会太多去改,绝大部分的工作都是在对应module的builde.gradle文件。
我们看一下app的build.gradle文件:
apply plugin: 'com.android.application' android { compileSdkVersion 25 buildToolsVersion "25.0.3" defaultConfig { applicationId "com.example.weber_zheng.myapplication" minSdkVersion 15 targetSdkVersion 25 versionCode 1 versionName "1.0" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) testCompile 'junit:junit:4.12' compile 'com.android.support:appcompat-v7:25.3.1' }
apply plugin表明应用的插件的类型,这里是com.android.application类型,而库工程则是com.android.library,如果是java项目,则是java。
Gradle中的plugin可以理解为一个已经定义好的模块,我们只要通过apply添加进来即可,对于Android开发来说,只要知道自己的module需要引入的插件是application还是library就可以了,当然,如果确实需要第三方插件,也可以导进来,类似bugtags这种插件。
android中就是Android插件的相关配置,我们很多操作基本都是在这里。
我们关注buildTypes,这里只有release一种编译类型,实际上,可以添加debug等其他自定义的类型。
在buildTypes中,minifyEnabled控制是否开启混淆,而proguardFiles指定了混淆文件,就是前面提到的proguard-rules.pro文件。
dependencies和build.gradle文件中的dependencies是一样的作用,如果我们想要编译工程,可以compile project(':projectName'),编译aar包,可以compile(name: 'aarName', ext: 'aar')。
一般build.gradle在配置完成后,都不会有太大的改动,而经常改动的地方就是添加依赖的时候。
我们可以在dependencies中添加依赖,依赖的类型常见是三种:jar,.so,aar和project。
默认的builde.gralde文件都会有这一句:compile fileTree(dir: 'libs', include: ['*.jar']),它表示编译libs目录下的.jar文件,所以如果我们有新的jar包,都放在libs目录里面,如果make工程都没反应,可以在Gradle那里点击同步。
但如果有需求,需要放在libs的子目录呢?
这时候是找不到这个.jar文件的,可以在compile中显示的指定.jar文件的具体路径,不过也可以更加简单点,fileTree的参数是一个Map,dir只能指定一个目录,但是include可以指定多个,因此可以修改为类似这样['*.jar'], ['test/*.jar']。
如果我们想要编译.so文件,就要指定jinLibs的目录,而这个动作是通过sourceSets类执行:
sourceSets {
main {
jniLibs.srcDirs = ['libs']//指定lib库目录
}
}
这里指定了jinLibs的目录是libs,因此我们可以将.so文件直接放在libs目录下。
Android开发中,不可避免的会导入各种第三方库,但如果第三方库本身也导入了和我们主工程一样的库,就会报错,因此要排除第三方库中重复的库:
compile('org.eclipse.paho:org.eclipse.paho.android.service:1.0.2') { exclude(group: 'com.google.android', module: 'support-v4') }
以上是创建一个APP时候,Android Studio默认的Gradle相关的内容,接下来我们来看一下在具体的生产环境中如何对应具体的需求修改build.gradle文件。
在应用开发中,正式环境和测试环境的ip地址肯定是不一样的,因此测试版本和正式版本在打包的时候需要切换对应的环境。
我们以前采用很笨的做法:设置一个静态变量,表示服务器的ip地址,然后有一个被注释掉的同名变量,表示测试地址,打包时候分别注释对应的地址。
很显然,这样的做法相当愚蠢。
所以,后来我们采取了另一种做法:设置一个debug开关,控制服务器地址的切换。
现在好多了,但还是需要去修改代码设置debug的值。
当然,我们可以在测试的时候,指定build的类型是debug,Android Studio的debug包和release包是有区分的,但很多第三方都要有签名才能测试,而debug包的默认签名是无法测试的,所以问题的症结就在于:如何打包都有正式签名的测试包和发布包,而且不需要修改代码。
Gradle可以通过buildTypes做到这一点:
buildTypes { debug { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' buildConfigField "int", "CONFIG_MODE", "1" buildConfigField "int", "LOG_SWITCH", "1" } release { signingConfig signingConfigs.myConfig minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' buildConfigField "int", "CONFIG_MODE", "2" buildConfigField "int", "LOG_SWITCH", "0" } }
我们可以添加buildConfigField,这里添加了一个CONFIG_MODE表示配置的模式,0表示relese,1表示debug,然后通过BuildConfig.CONFIG_MODE获取这个值,在代码中进行判断,这样只要在这里进行设置就可以:
BuildConfig是自动生成的文件,它里面存放build.gradle配置的值:
public final class BuildConfig { public static final boolean DEBUG = Boolean.parseBoolean("true"); public static final String APPLICATION_ID = "com.wenjiang.http"; public static final String BUILD_TYPE = "debug"; public static final String FLAVOR = ""; public static final int VERSION_CODE = 1; public static final String VERSION_NAME = "1.0"; // Fields from build type: debug public static final int CONFIG_MODE = 1; public static final int LOG_SWITCH = 1; }
看到这里,我们想到了可以更好的精简代码。
我们可以重新添加一个CONFIG_IP,这样代码中就不需要设置ip常量,也不用写判断语句,不过要注意String类型要添加一对双引号,这样生成的时候才会是正确的字符类型。
buildTypes还有其他的属性可以配置,例如shrinkResources可以用于指定删除无效的Resource。
但是这个属性有个问题:我们应用中有些资源是主题指定资源,是在确认加载主题的时候才会去添加,类似这样的使用方式:
getResources().getIdentifier(key, “drawable”,getPackageName()));
所以这些资源在编译打包的时候,因为没有引用,会被认为是无用资源而被移除,这样肯定是有问题的。
但是目前我们无法还原真实的场景,所以暂时跳过这部分。
buildTypes还可以指定生成的apk的名字后缀,这样我们就能在同一部测试机上同时安装debug和release版本,这个可以通过applicationIdSuffix ".debug"进行指定。
applicationId用于标识我们的应用,默认情况下是应用的包名,不分debug和release,所以在两个环境切换的时候,都需要重新安装,如果不想重新安装,就要为其中一个版本指定后缀。
同样还有一个versionNameSuffix可以在版本名字后面添加后缀。
应用在正式发布后,可以通过debuggable,jniDebuggable和renderscriptDebuggable设置是否可调试。
这里有一个zipAlignEnabled属性要值得注意。
这个属性设置是否对APK包执行ZIP对齐优化,而这个跟应用程序运行时优化有关。
ZipAlign在Android 1.6的时候引入,能够对打包的应用程序进行优化,使Android操作系统与应用程序间的交互作用更有效率,因此运行得更快。
它到底是怎么做到的呢?
ZipAlign对apk文件中未压缩的数据在4个字节边界上对齐,这样Android系统就可以通过调用mmap函数读取文件,这样进程就可以通过映射同一个普通文件实现内存共享,因此可以像访问普通内存一样对文件进行访问,不必调用read()和write()等操作。
而4个字节边界上对齐,编译器就能按照4个字节为单位进行读取,CPU就能对变量进行高效快速的访问。
Android系统中的Davlik虚拟机使用的是自己专有的DEX格式,DEX的结构是紧凑的,而对齐可以进一步优化。
因此,ZipAlign能够加快系统从APK文件中读取资源,并且减少这个过程中耗费的内存。
buildTypes可以设置renderScript的相关属性,而renderScript一般的应用都不会涉及到,常见的例子就是使用到高斯模糊的时候,为了优化高斯模糊效果和性能,所以这部分就跳过。
buildTypes还可以通过proguardFiles来指定多个混淆文件,但这个需求也是比较少用到。
signingConfig用于指定签名的配置文件,我们可以在build.gradle文件中设置对应的配置:
signingConfigs {
myConfig {
storeFile file("android.keystore") //签名文件
storePassword "****"
keyAlias "android"
keyPassword "****" //签名密码
}
然后通过signingConfig来指定。
如果新的buildTypes大部分和另一个buildTypes一样,我们可以通过jnidebug.initWith(buildTypes.debug)来复制debug的内容,然后在jnidebug中设置自己的属性。
有关buildTypes的大概内容就是这样,常用的属性和相关的配置,意义也大概介绍了,一般的应用也大概只需要了解到这种程度,至少我们的应用就是这样。