一、Gradle简介
Gradle是Android现在主流的编译工具,虽然在Gradle出现之前和之后都有对应更快的编译工具出现,但是Gradle的有时就在于它是亲儿子,Gradle确实比较慢,这和它的编译过程有关,但是现在的Gradle编译速度已经有了成倍提高。除此之外,相对其他编译工具,最重要的,他和Android Studio的关系非常紧密,可以说对于一些简单的程序我们几乎不需要任何代码上的配置只使用Android Studio就可以完成编译和运行。
但是对于一些比较复杂的,特别是多人团队合作的项目我们会需要一些个性化的配置来提高我们的开发效率。比如我们要自定义编译出的apk包的名字、对于一些特殊产品我们可能会要用同一个项目编译出免费版和付费版的apk。这些高级的功能都需要我们对配置代码进行自定义地修改。
在很多情况下我们都是使用的Android Studio来build、debug项目。Android Studio能满足我们开发的大多数需求,但是某些情况下命令行能够让我们编译的效率更高,过程更明朗,一些高级的配置也需要熟悉命令行才能够使用,比如在服务器编译,某些项目初始化的时候如果直接交给Android Studio,它会一直Loading,你都不知道它在干嘛,但是用命令行你就知道它卡在了哪个环节,你只需要修改某些代码,马上就能够编译过了。
二、使用Gradle可以做什么
1.自定义编译输出文件格式。
2.hook Android编译过程。
3.配置和改善Gradle编译速度。
三、Gradle的编译周期
每个项目的编译至少有一个Project,一个build.gradle就代表一个project,每个project里面包含了多个task,task里面又包含很多action,action是一个代码块,里面包含了需要被执行的代码。
在编译过程中,Gradle会根据build相关文件,聚合所有的project和task,执行task中的action。因为build.gradle文件中的task非常多,先执行哪个后执行哪个需要一种逻辑老保证。这种逻辑就是依赖逻辑,几乎所有的Task都需要依赖其他task来执行,没有被依赖的task会首先被执行。所以到最后所有的task都需要依赖其他task来执行,没有被依赖的task会首先被执行。所以到最后所有的Task会构成一个有向无环图(DAG Directed Acyclic Graph)的数据结构。
编译过程分为三个阶段:
(1).初始化阶段:创建Project对象,如果有多个build.gradle,也会创建多个project。
(2).配置阶段:在这个阶段,会执行所有的编译脚本,同时还会创建project的所有的task,为后一个阶段做准备。
(3).执行阶段:在这个阶段,gradle会根据传入的参数决定如何执行这些task。真正action的执行代码就在这里。
1.Gradle Files
对于一个gradle项目,最基础的文件配置如下:
MyApp
|--- build.gradle
|--- settings.gradle
|--- app
|--- build.gradle
一个项目有一个setting.gradle、包括一个顶层的build.gradle文件、每个Module都有自己的一个build.gradle文件。
(1)setting.gradle:这个setting文件定义了哪些module应该被加入到编译过程,对于单个module的项目可以不用需要这个文件,但是对于multimodule的项目我们就需要这个文件,否则gradle不知道要加载哪个项目。这个文件的代码在初始化阶段就会被执行。
(2)顶层的build.gradle:顶层的build.gradle文件的配置最终会被用用到所有项目总。它典型的配置如下:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.2.3'
}
}
allprojects{
repositories{
jcenter()
}
}
a.buildscript:定义了Android编译工具的类路径。repositories中,jCenter是一个著名的Maven仓库。
b.allprojects:中定义的属性会被应用到所有moudle中,但是为了保证每个项目的独立性,我们一般不会在这里面操作太多共有的东西。
c.每个项目单独的build.gradle:针对每个module的配置,如果这里的定义的选项和顶层build.gradle定义的相同,后者会被覆盖。典型的配置内容如下:
apply plugin: 'com.android.application'
android {
compileSdkVersion 25
buildToolsVersion "25.0.0"
defaultConfig {
applicationId "com.zhangmiao.webviewdemo"
minSdkVersion 19
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'])
compile 'com.android.support:appcompat-v7:25.+'
}
(1)apply plugin:第一行代码应用了Android程序的gradle插件,作为Android的应用程序,这一步是必须的,因为plugin中提供了Android编译、测试、打包等等的所有task。
(2)android:这是编译文件中最大的代码块,关于android的所有特殊配置都在这里,这就是前面的声明的plugin提供的。
a.defaultConfig就是程序的默认配置,注意,如果在AndroidMainfest.xml里面定义了与这里相同的属性,会以这里的为主。
b.这里最有必要要说明的是applicationId的选项:在曾经定义的AndroidManifest.xml中,那里定义的包名有两个用途:一个是作为程序的唯一识别ID,防止在同一个手机装两个一样的程序;另一个就是作为R资源类的包名。以前修改ID会导致所有引用R资源类的地方都要修改。但是现在我们如果修改applicationId只会修改当前程序的IO,而不会去修改源码中资源文件的应用。
(3)buildTypes:定义了编译类型,针对每个类型可以有不同的编译配置,不同的编译配置对应的有不同的编译命令。默认的有debug、release的类型。
(4)dependencies:是属于gradle的依赖配置。它定义了当前项目需要依赖的其他库。
2.Gradle Wrapper
Gradle不断的在发展,新的版本难免会对以往的项目有一些向后兼容性的问题。这个时候,gradle wrapper就应运而生了。
gradle wrapper包含一些脚本文件和针对不同系统下面的运行文件。wrapper有版本区分,但是并不需要你手动去下载,当你运行脚本的时候,如果本地没有会自动下载对应版本文件。
在不同操作系统下面执行的脚本不同,在Mac系统下执行./gradlew...,在windows下执行gradle.bat进行编译。
如果直接从eclipse中的项目转换过来的,程序并不会自动创建wrapper脚本,需要手动创建。在命令行输入以下命令即可:
gradle wrapper --gradle-version 2.4
它会创建如下目录结构:
myapp/
|--- gradlew
|--- gradlew.bat
|--- gradle/wrapper/
|--- gradle-wrapper.jar
|--- gradle-wrapper.properties
3.Gradle basics
Gradle 会根据build文件的配置生成不同的task,可以直接单独执行每一个task。通过./gradlew tasks列出所有task。如果通过同事还想累出每个task对应依赖的其他task,可以使用./gradlew tasks -all。
当在Android Studio点击build,rebuild,clean菜单的时候,执行的就是一些gradle task。
4.Android tasks
有四个基本的task,Android继承他们分别进行了自己的实现:
1.assemble:对所有的buildType生成apk包。
2.clean:移除所有的编译输出文件,比如apk。
3.check:执行lint检测编译。
4.build:同时执行assemble和check命令。
这些都是基本的命令,在实际项目中会根据不同的配置,会对这些task设置不同的依赖。比如默认的assmeble会依赖assembleDebug和assembleRelease,如果直接执行asseme,最后会编译debug,和release的所有版本出来。如果我们只需要编译debug版本,我们可以运行assembleDebug。
除此之外还有一些常用的新增的其他命令,比如install命令。会将编译后的apk安装到连接的设备。
我们运行的许多命令除了会输出到明亮航,还会在build文件夹下生成一份运行报告。
四、Configuration
1.BuildConfig
常用的用法就是通过BuildConfig.DEBUG来判断当前版本是否是debug版本,如果是就会输出一些只有在debug环境下才会执行的操作。这个类就是由gradle根据配置文件生成的。为什么gradle可以直接生成一个Java字节码类,这就得益于我们的gradle的编写语言是Groovy,Groovy是一种JVM语言,JVM语言的特征就是,虽然编写的语法不一样,但是它们最终都会编成JVM字节码文件。同是JVM怨言的还有Scale,Kotlin等等。
这个功能非常强大,我们可以通过在这里设置一些key-value对,这些key-value对在不同编译类型的apk下的值不同。
比如,可以为debug和release梁总环境定义不同的服务器。
android {
buildTypes {
debug {
buildConfigField "String", "API_URL", ""http://test.example.com/api""
buildConfigField "boolean", "LOG_HTTP_CALLS", "true"
}
release {
buildConfigField "String", "API_URL", ""http://example.com/api""
buildConfigField "boolean", "LOG_HTTP_CALLS", “false”
}
}
}
除此之外,还可以为不同的编译类型设置不同的资源文件,比如:
android {
buildTypes {
debug {
resValue "string", "app_name", "Example DEBUG"
}
release {
resValue "string", "app_name", "Example"
}
}
}
2.Repositories
Repositories就是代码仓库,平时添加的一些dependency就是从这里下载的,Gradle支持三种类型的仓库:Maven,lvy和一些静态文件或者文件夹。在编译的执行阶段,gradle将会从仓库中取出对应需要的依赖文件,当然,gradle本地也会有自己的缓存,不会每次都去取这些依赖。
gradle支持多种Maven仓库,一般我们就是用共有的jCenter就可以了。有一些项目,可能是一些公司私有的仓库中的,这时候我们需要手动加入仓库连接:
repositories {
maven {
url "http://repo.acmecorp.com/maven2"
}
}
如果仓库有密码,也可以同时传入用户名和密码
repositories {
maven {
url "http://repo.acmecorp.com/maven2"
credentials {
username 'user'
password 'secretpassword'
}
}
}
我们也可以使用相对路径配置本地仓库,我们可以通过配置项目中存在的静态文件夹做作本地仓库:
repositories {
flatDir {
dirs 'aars'
}
}
3.Dependencies
我们在引用库的时候,每个库名称包含三个元素:组名:库名称:版本号,如下:
dependencies {
compile 'com.google.code.gson:gson:2.3'
compile 'com.squareup.retrofit:retrofit:1.9.0'
}
如果要保证我们依赖的库始终处于最新状态,可以通过通配符的方式,比如:
dependencies {
compile 'com.android.support:support-v4:22.2.+'
compile 'com.android.support:appcompat-v7:22.2+'
compile 'com.android.support:recyclerview-v7:+'
}
但是一般不要这么做,这样做除了每次编译都要去做网络请求查看是否有新版本导致编译国漫外,最大的弊病在于我们使用过的版本很可能是测试版,性能得不到保证,所以,在我们引用库的时候一定要指名依赖版本。
五、Local dependencies
1.File dependencies
通过files()方法可以添加文件依赖,如果有很多jar文件,我们也可以通过fileTree()方法添加一个文件夹,除此之外,还可以通过通配符的方式添加,如下:
dependencies {
compile fileTree(dir: 'libs', include: {'*.jar'})
}
2.Native libraries
配置本地.so库。在配置文件中做如下配置,然后在对应位置建立文件夹,加入对应平台的.so文件。
android {
sourceSets.main {
jniLibs.srcDir 'src/main/libs'
}
}
3.Library projects
如果我们要写一个library项目让其他的项目引用,我们的build.gradle的plugin就不能是android plugin了,需要引用如下plugin:
apply plugin: 'com.android.library'
引用事务阿时候在setting文件中include即可。
如果不方便引用项目,需要通过文件的形式引用,也可以将项目打包成aar文件,注意,这种情况下,我们在项目下面新建arrs文件夹,并在build.gradle文件中配置仓库:
repositories {
flatDir {
dirs 'aars'
}
}
当需要引用里面的某个项目时,通过如下方式引用:
dependencies {
compile(name:'libraryname',ext:'aar')
}
4.Build Version
在开发中可能会有这样的需求:
(1)需要在debug和release两种情况下配置不同的服务器地址。
(2)当打市场渠道包的时候,可能需要打免费版、收费版,或者内部版、外部版的程序。
(3)渠道首发包通常需要要求在欢迎页添加渠道的logo,等等。
(4)为了让市场版和debug版同时存在于一个手机,我们需要编译的时候自动给debug版本不一样的包名。
这些需求都需要在编译的时候动态根据当前的编译类型输出不同样式的apk文件。这时候就是buildType大展身手的时候了。
5.Build Type
android默认的带有Debug和Release两种编译类型。比如现在有一个新的stating的编译类型:
android {
buildTypes {
staging.initWith(buildTypes.debug)
staging {
applicationIdSuffix ".staging"
versionNameSuffix "-staging"
debuggable = false
}
}
}
6.Source sets
每当创建一个新的build type的时候,gradle默认都会创建一个新的source set。可以建立与main文件夹同级的文件夹,根据编译类型的不同可以选择对某些源码直接金子能够替换。
除了代码可以替换,资源文件也可以替换。
除此之外,不同编译类型的项目,依赖都可以不同,比如,如果需要在staging和debug两个版本中使用不同的log框架,可以这样配置:
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:22.2.0'
debugCompile 'de.mindpipe.android:android-logging-log4j:1.0.3'
}
7.Product flavors
前面都是针对同一份源码编译同一个程序的不同类型,如果需要针对同一份源码编译不同的程序(包名也不同),比如免费版和收费版。我们就需要Product flavors。
注意,Product flavors和Build Type是不一样的,而且他们的属性也不一样。所有的Product flavor版本和defaultConfig共享所有属性。
像Build type一样,product flavor也可以有自己的source set文件夹。除此之外,product falvor和build type可以结合,它们的文件夹里面的文件优先级甚至高于单独的build type和product flavor文件夹的优先级。如果想对于blue类型的release版本有不同的图标,可以建立一个文件夹叫blueRelease,注意,这个顺序不能错,一定是flavor+buildType的形式。
更复杂的情况下,可能需要多个product的维度进行组合,比如想要color和price两个维度去构建程序。这个时候就需要使用flavorDimensions:
android {
flavorDimensions "color", "price"
productFlavors {
red {
flavorDimension "color"
}
blue {
flavorDimension "color"
}
free {
flavorDimension "price"
}
paid {
flavorDimension "price"
}
}
}
根据我们的配置。再次查看我们的task,发现多了这些task:
blueFreeDebug,blueFreeRelease,bluePaidDebug,bluePaidRelease,redFreeDebug,redFreeRelease,redPaidDebug,redPaidRelease。
8.Resource merge priority
Build type > Flavor > Main > Dependencies
在Build Type中定义的资源优先级最大,在Library中定义的资源优先级最低。
9.Signing configurations
如果打包市场版的时候,需要输入keystore数据。如果是debug版本,系统默认会配置这些信息。这些信息在gradler中都配置在signingConfigs中。
android {
signingConfigs {
staging.initWith(signingConfigs.debug)
release {
storeFile file("release.keystore")
storePassword "secretpassword"
keyAlias "gradleforandroid"
keyPassword "secretpassword"
}
}
}
配置之后需要在build type中直接使用:
android {
buildTypes {
release {
signingConfig signingConfigs.release
}
}
}
六、Optimize
1.Speeding up multimodule builds
可以通过以下方式加快gradle的编译:
(1)开启并行编译:在项目根目录下面的gradle.properties中设置:
org.gradle.parallel = true
(2)开启编译守护进程:该进程在第一次启动后会一直存在,当进行第二次编译的时候,可以重用该进程。同样是在gradle.properties中设置。
org.gradle.daemon = true
(3)加大可用编译内存
org.gradle.jvmargs=-Xms256m -Xmx1024m
2.Reducing apk file
在编译的时候,可能会有很多资源并没有用到,此时就可以通过shrinkResources来优化我们的资源文件,除去那些不必要的资源。
android {
buildTypes {
release {
minifyEnabled = true
shrinkResources = true
}
}
}
如果我们需要查看该命令帮我们减少了多少无用的资源,也可以通过运行shrinkReleaseResources命令来查看log。
某些情况下,一些资源是需要通过动态加载的方式载入的,这时候我也需要像Progard一样对我们的资源进行keep操作。方法就是在res/raw/下建立一个keep.xml文件,通过如下方式keep资源:
:app:shrinkReleaseResources
Removed unused resources: Binary resource data reduced from 433KB to 354KB: Removed 18%
3.Manual shrinking
对一些特殊的文件或者文件夹,比如国际化的资源文件、屏幕适配资源,如果我们已经确定了某种型号,而不需要重新适配,我们可以直接去掉不可能会被适配的资源。这在为厂商适配机型定制app的时候是很有用的。做法如下:
比如我们可能有非常多的国际化的资源,如果我们应用场景只用到了English,Danish,Dutch的资源,我们可以直接指定我们的resConfig:
<?xml version="1.0" encoding="utf-8"?> <resources xmlns:tools="http://schemas.android.com/tools" tools:keep="@layout/keep_me,@layout/also_used_*"/>
对于尺寸文件我们也可以这样做:
android {
defaultConfig {
resConfigs "hdpi","xhdpi","xxhdpi","xxxhspi"
}
}
4.Profiling
当我们执行所有task的时候我们都可以通过添加--profile参数生成一份执行报告在reports/profile中。
可以通过报告看出哪个项目耗费的时间最多,哪个环节耗费的时间最多。
七.Practice
1.Groovy
Groovy是一门JVM语言,也就是,Groovy的代码最终也会被编译成JVM字节码,交给虚拟机去执行,我们也可以直接反编译这些字节码文件。
1.1.变量
在groovy中,没有固定的类型,变量可以通过def关键字引用,比如:
def name = 'Andy'
我们通过单引号引用一串字符串的时候这个字符串只是单纯的字符串,但是如果使用双引号引用,在字符串里面还支持插值操作,
def name = 'Andy'
def greeting = "Hello, $name!"
1.2.方法
类似python一样,通过def关键字定义一个方法。方法如果不执行返回值,默认返回最后一行代码的值。
def square(def num) {
num * num
}
square 4
1.3.类
Groovy也是通过Groovy定义一个类:
class MyGroovyClass {
String greeting
String getGreeting() {
return 'Hello!'
}
}
在Groovy中,默认所有的类和方法都是public的,所有类的字段都是private的。
和java一样,通过new关键字得到类的实例,使用def接收对象的引用:def instance = new MyGroovyClass()。
而且在类中声明的字段都默认会生成对应的setter,getter方法。所以上面的代码可以直接调用instance.setGreeting 'Hello,Groovy!',注意,groovy的方法调用时可以没有括号的,而且也不需要分号结尾。除此之外,我们甚至也可以直接调用。
可以直接通过instance.greeting这样的方式拿到字段值,但其实这也会通过其get方法,而且不是直接拿到这个值。
1.4.map、collections
在Groovy中,定义一个列表是这样的:
List list = [1,2,3,4,5]
遍历一个列表是这样的:
list.each() { element ->
println element
}
定义一个map是这样的:
Map pizzaPrices = [margherita:10,pepperoni:12]
获取一个map值是这样的:
pizzaPrices.get('pepperoni')
pizzaPrices['pepperoni']
1.5.闭包
在Groovy中有一个闭包的概念。闭包可以理解为就是Java中的匿名内部类。闭包支持类似lamda形式的语法调用。如下:
def square = { num ->
num * num
}
square 8
如果只有一个参数,甚至可以忽略这个参数,默认使用it作为参数,最后代码是这样的:
Closure square = {
it * it
}
square 16
android,dependencies这些后面紧跟的代码块,都是一个闭包而已。
2.Groovy in Gradle
2.1.apply
apply plugin: 'com.android.application'
这段代码其实就是调用了project对象的apply方法,传入了一个以plugin为key的map。完整写出来就是:
project.apply([plugin: 'com.android.application'])
2.2.dependencies
dependencies {
compile 'com.google.code.gson:gson:2.3'
}
实际调用的时候会传入一个DependencyHandler的闭包,代码如下:
project.denpendencies({
add('compile', 'com.google.code.gson:gson:2.3', {
//Configuration statements
})
})
3.Task
3.1.创建一个task
task hello {
println 'Hello, world!'
}
运行该task
./gradlew hello
gradle的生命周期分但不,初始化,配置和执行。上面的代码在配置过程就已经执行了,所以,打印出的字符串发生在该任务执行之前,如果要在执行阶段才执行任务中的代码应该如下配置:
task hello << { println 'Hello, world!' }
3.2.添加Action
task包含系列的action,当task被执行的时候,所有的action都会被依次执行。如果我们要加入自己的action,我们可以通过复写doFirst()和doLast()方法。
task hello {
println 'Configuration'
doLast {
println 'Goodbye'
}
doFirst {
println 'Hello'
}
}
打印出来如下:
$ gradlew hello
Configuration
:hello
Hello
Goodbye
3.3.Task依赖
task之间的关系就是依赖关系,关于Task的依赖有两种,must RunAfter和dependsOn。比如:
task task1 << { printfln 'task1' } task task2 << { println 'task2' } task2.mustRunAfter task1
和
task task1 << { printfln 'task1' } task task2 << { println 'task2' } task2.dependsOn task1
区别是,运行的时候前者必须要都按顺序加入gradlew task2 task1执行才可以顺利执行,否则单独执行每个任务,后者只需要执行gradlew task2即可同时执行两个任务。
4.Practice
4.1.keystore保护
android {
signingConfigs {
staging.initWith(signingConfigs.debug)
release {
storeFile file("release.keystore")
storePassword "secretpassword"
keyAlias "gradleforandroid"
keyPassword "secretpassword"
}
}
}
这里直接将store的密码明文写在这里对于产品的安全性来说不太好,特别是如果该源码开源,别人就可以用你的id去发布app。对于这种情况,我们需要构建一个动态加载任务,在编译release源码的时候从本地文件(未加入git)获取keystore信息,如下:
task getReleasePassword << { def password = '' if (rootProject.file('private.properties').exists()) { Properties properties = new Properties(); properties.load( rootProject.file ('private.properties').newDataInputStream()) password = properties.getProperty('release.password') } }
还可以设置一个保险措施,万一没有找到对应的文件需要用户从控制太输入密码
if (!password?.trim()) {
password = new String(System.console().readPassword
("
What's the secret password?"))
}
最后设置最终值。
然后设置release任务依赖我们刚刚设置的任务,
tasks.whenTaskAdded { theTask ->
if (theTask.name.equals("packageRelease")) {
theTask.dependsOn "getReleasePassword"
}
}
4.2.通过hook Android编译插件重命名apk
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
def file = output.outputFile
output.outputFile = new File(file.parent,
file.name.replace(".apk", "-${variant.versionName}.apk"))
}
}
最后编译出来的apk名字类似app-debug-1.0.apk
参考文章:http://www.jianshu.com/p/9df3c3b6067a