赞
踩
在解析 Gradle 的编译过程之前我们需要理解在 Gradle 中非常重要的两个对象。Project和Task。
每个项目的编译至少有一个 Project,一个 build.gradle
就代表一个project
,每个project
里面包含了多个task
,task 里面又包含很多action
,action
是一个代码块,里面包含了需要被执行的代码。
> 你还在为开发中频繁切换环境打包而烦恼吗?快来试试 Environment Switcher 吧!使用它可以在app运行时一键切换环境,而且还支持其他贴心小功能,有了它妈妈再也不用担心频繁环境切换了。[https://github.com/CodeXiaoMai/EnvironmentSwitcher](https://github.com/CodeXiaoMai/EnvironmentSwitcher)
在编译过程中, Gradle 会根据 build 相关文件,聚合所有的project
和task
,执行task 中的 action。因为 build.gradle
文件中的task
非常多,先执行哪个后执行那个需要一种逻辑来保证。这种逻辑就是依赖逻辑,几乎所有的Task 都需要依赖其他 task 来执行,没有被依赖的task 会首先被执行。所以到最后所有的 Task 会构成一个 有向无环图(DAG Directed Acyclic Graph)的数据结构。
编译过程分为三个阶段:
刚刚我们提到Gradle 编译的时候的一些相关文件,下面我们挨个解析一下这些文件。
对于一个gradle 项目,最基础的文件配置如下:
一个项目有一个setting.gradle
、包括一个顶层的 build.gradle
文件、每个Module 都有自己的一个build.gradle
文件。
include ':app', ':volley', ':vitamio'
build.gradle
文件的配置最终会被应用到所有子moudle中。它典型的配置如下:
/**
* buildscript:定义了 Android 编译工具的类路径。
*/
buildscript {
/**
* repositories中,jCenter是一个著名的 Maven 仓库。
*/
repositories {
jcenter()
}
/**
* 这是gradle的依赖
*/
dependencies {
/**
* 注意:不要把你的应用程序的依赖写到这里;他们应该在自己的模块build.gradle文件中配置
*/
classpath 'com.android.tools.build:gradle:2.1.2'
}
}
/**
* allprojects:中定义的属性会被应用到所有 moudle 中,但是为了保证每个项目的独立性,我们一般不会在这里面操作太多共有的东西。
*/
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
build.gradle
定义的相同,后者会被覆盖。典型的配置内容如下:/**
* apply plugin:第一行代码应用了Android 程序的gradle插件,作为 Android 的应用程序,
* 这一步是必须的,因为plugin中提供了Android 编译、测试、打包等等的所有task。
*/
apply plugin: 'com.android.application'
/**
* android:这是编译文件中最大的代码块,关于 android 的所有特殊配置都在这里,
* 这就是由我们前面的声明的 plugin 提供的。
*/
android {
compileSdkVersion 24
buildToolsVersion "24.0.3"
/**
* defaultConfig就是程序的默认配置,
* 注意,如果在AndroidMainfest.xml里面定义了与这里相同的属性,会以这里的为主。
* 这里最有必要要说明的是applicationId的选项:
* 在我们曾经定义的AndroidManifest.xml中,那里定义的包名有两个用途:
* 一个是作为程序的唯一识别ID,防止在同一手机装两个一样的程序;
* 另一个就是作为我们R资源类的包名。在以前我们修改这个ID会导致所有引用R资源类的地方都要修改。
* 但是现在我们如果修改applicationId只会修改当前程序的ID,而不会去修改源码中资源文件的引用。
*/
defaultConfig {
applicationId "com.xiaomai.myproject"
minSdkVersion 15
targetSdkVersion 24
versionCode 1
versionName "1.0"
}
/**
* buildTypes:定义了编译类型,针对每个类型我们可以有不同的编译配置,
* 不同的编译配置对应的有不同的编译命令。默认的有debug、release 的类型。
*/
buildTypes {
debug{
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
ndk {
abiFilters "armeabi", "armeabi-v7a"
}
}
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
assets.srcDirs = ['assets']
}
}
}
/**
* dependencies:是属于gradle 的依赖配置。它定义了当前项目需要依赖的其他库。
*/
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
testCompile 'junit:junit:4.12'
compile project(path: ':volley')
compile project(':vitamio')
compile 'com.android.support:appcompat-v7:24.2.1'
compile 'com.android.support:recyclerview-v7:24.2.1'
compile 'pl.droidsonroids.gif:android-gif-drawable:1.2.2'
}
Gradle 不断的在发展,新的版本难免会对以往的项目有一些向后兼容性的问题,这个时候,gradle wrapper
就应运而生了。
gradlw wrapper 包含一些脚本文件和针对不同系统下面的运行文件。wrapper 有版本区分,但是并不需要你手动去下载,当你运行脚本的时候,如果本地没有会自动下载对应版本文件。
在不同操作系统下面执行的脚本不同,在 Mac 系统下执行./gradlew ...
,在windows 下执行gradle.bat
进行编译。
如果你是直接从eclipse 中的项目转换过来的,程序并不会自动创建wrapper
脚本,我们需要手动创建。在命令行输入以下命令即可
gradle wrapper --gradle-version 2.4
它会创建如下目录结构:
wrapper 就是我们使用命令行编译的开始。下面我们看看 wrapper 有什么样的作用。
Android tasks
有四个基本的 task, Android 继承他们分别进行了自己的实现:
lint
检测编译。assemble
和check
命令这些都是基本的命令,在实际项目中会根据不同的配置,会对这些task 设置不同的依赖。比如 默认的 assmeble 会依赖 assembleDebug 和assembleRelease,如果直接执行assmeble
,最后会编译debug,和release 的所有版本出来。如果我们只需要编译debug 版本,我们可以运行assembleDebug
。
除此之外还有一些常用的新增的其他命令,比如 install命令,会将编译后的apk 安装到连接的设备。
我们运行的许多命令除了会输出到命令行,还会在build
文件夹下生产一份运行报告。比如check
命令会生成lint-results.html.
在build/outputs
中。
Repositories 就是代码仓库,这个相信大家都知道,我们平时的添加的一些 dependency 就是从这里下载的,Gradle 支持三种类型的仓库:Maven,Ivy和一些静态文件或者文件夹。在编译的执行阶段,gradle 将会从仓库中取出对应需要的依赖文件,当然,gradle 本地也会有自己的缓存,不会每次都去取这些依赖。
gradle 支持多种 Maven 仓库,一般我们就是用共有的jCenter
就可以了。
有一些项目,可能是一些公司私有的仓库中的,这时候我们需要手动加入仓库连接:
如果仓库有密码,也可以同时传入用户名和密码
我们也可以使用相对路径配置本地仓库,我们可以通过配置项目中存在的静态文件夹作为本地仓库:
我们在引用库的时候,每个库名称包含三个元素:组名:库名称:版本号
,如下:
如果我们要保证我们依赖的库始终处于最新状态,我们可以通过添加通配符的方式,比如:
但是我们一般不要这么做,这样做除了每次编译都要去做网络请求查看是否有新版本导致编译过慢外,最大的弊病在于我们使用过的版本很很困难是测试版,性能得不到保证,所以,在我们引用库的时候一定要指名依赖版本。
Local dependencies
File dependencies
通过files()
方法可以添加文件依赖,如果有很多jar文件,我们也可以通过fileTree()
方法添加一个文件夹,除此之外,我们还可以通过通配符的方式添加,如下:
Library projects
如果我们要写一个library项目让其他的项目引用,我们的bubild.gradle的plugin 就不能是andrid plugin了,需要引用如下plugin
apply plugin: 'com.android.library'
例如:Vitamio的build.gradle就是这样配置的。
引用的时候在setting文件中include
即可。
如果我们不方便直接引用项目,需要通过文件的形式引用,我们也可以将项目打包成aar
文件,注意,这种情况下,我们在项目下面新建arrs
文件夹,并在build.gradle 文件中配置 仓库:
当需要引用里面的某个项目时,通过如下方式引用:
在开发中我们可能会有这样的需求:
这些需求都需要在编译的时候动态根据当前的编译类型输出不同样式的apk文件。这时候就是我们的buildType
大展身手的时候了。
android 默认的带有Debug和Release两种编译类型。比如我们现在有一个新的statging
的编译类型
每当创建一个新的build type 的时候,gradle 默认都会创建一个新的source set。我们可以建立与main
文件夹同级的文件夹,根据编译类型的不同我们可以选择对某些源码直接进行替换。
除了代码可以替换,我们的资源文件也可以替换
除此之外,不同编译类型的项目,我们的依赖都可以不同,比如,如果我需要在staging和debug两个版本中使用不同的log框架,我们这样配置:
前面我们都是针对同一份源码编译同一个程序的不同类型,如果我们需要针对同一份源码编译不同的程序(包名也不同),比如 免费版和收费版。我们就需要Product flavors
。
注意,Product flavors和Build Type是不一样的,而且他们的属性也不一样。所有的 product flavor 版本和defaultConfig 共享所有属性!
像Build type 一样,product flavor 也可以有自己的source set
文件夹。除此之外,product flavor 和 build type 可以结合,他们的文件夹里面的文件优先级甚至高于 单独的built type 和product flavor 文件夹的优先级。如果你想对于 blue类型的release 版本有不同的图标,我们可以建立一个文件夹叫blueRelease
,注意,这个顺序不能错,一定是 flavor+buildType 的形式。
更复杂的情况下,我们可能需要多个product 的维度进行组合,比如我想要 color 和 price 两个维度去构建程序。这时候我们就需要使用flavorDimensions
:
根据我们的配置,再次查看我们的task,发现多了这些task:
在Build Type中定义的资源优先级最大,在Library 中定义的资源优先级最低。
如果我们打包市场版的时候,我们需要输入我们的keystore数据。如果是debug 版本,系统默认会帮我们配置这些信息。这些信息在gradle 中都配置在signingConfigs
中。
配置之后我们需要在build type中直接使用
多模块并行编译提高编译速度
可以通过以下方式加快gradle 的编译:
gradle.properties
中设置org.gradle.parallel=true
gradle.properties
中设置。org.gradle.daemon=true
org.gradle.jvmargs=-Xms256m -Xmx1024m
在编译的时候,我们可能会有很多资源并没有用到,此时就可以通过shrinkResources
来优化我们的资源文件,除去那些不必要的资源。
如果我们需要查看该命令帮我们减少了多少无用的资源,我们也可以通过运行shrinkReleaseResources
命令来查看log.
某些情况下,一些资源是需要通过动态加载的方式载入的,这时候我也需要像 Progard 一样对我们的资源进行keep操作。方法就是在res/raw/
下建立一个keep.xml
文件,通过如下方式 keep 资源:
对一些特殊的文件或者文件夹,比如 国际化的资源文件、屏幕适配资源,如果我们已经确定了某种型号,而不需要重新适配,我们可以直接去掉不可能会被适配的资源。这在为厂商适配机型定制app的时候是很用的。做法如下:
比如我们可能有非常多的国际化的资源,如果我们应用场景只用到了English,Danish,Dutch的资源,我们可以直接指定我们的resConfig
:
对于尺寸文件我们也可以这样做
当我们执行所有task的时候我们都可以通过添加--profile
参数生成一份执行报告在reports/profile
中。示例如下:
我们可以通过这份报告看出哪个项目耗费的时间最多,哪个环节耗费的时间最多。
在开发的过程中,我们可能会遇到很多情况需要我们能够自己定义task,在自定义task 之前,我们先简单看看groovy 的语法。
我们前面看到的那些build.gradle 配置文件,和xml 等的配置文件不同,这些文件可以说就是可以执行的代码,只是他们的结构看起来通俗易懂,和配置文件没什么两样,这也是Google 之所以选择Groovy 的原因。除此之外,Groovy 是一门JVM 语言,也就是,Groovy 的代码最终也会被编译成JVM 字节码,交给虚拟机去执行,我们也可以直接反编译这些字节码文件。
我们这里简单地说一下 groovy 一些语法。
在groovy 中,没有固定的类型,变量可以通过def
关键字引用,比如:
def name = 'Andy'
我们通过单引号引用一串字符串的时候这个字符串只是单纯的字符串,但是如果使用双引号引用,在字符串里面还支持插值操作,
- def name = 'Andy'
- def greeting = "Hello, $name!"
类似 python 一样,通过def
关键字定义一个方法。方法如果不指定返回值,默认返回最后一行代码的值。
- def square(def num) {
- num * num
- }
- square 4
Groovy 也是通过Groovy 定义一个类:
- class MyGroovyClass {
- String greeting
- String getGreeting() {
- return 'Hello!'
- }
- }
pulic
的,所有类的字段都是private
的;new
关键字得到类的实例,使用def
接受对象的引用:def instance = new MyGroovyClass()
instance.setGreeting 'Hello, Groovy!'
,注意,groovy 的方法调用是可以没有括号的,而且也不需要分号结尾。除此之外,我们甚至也可以直接调用;instance.greeting
这样的方式拿到字段值,但其实这也会通过其get方法,而且不是直接拿到这个值。在 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']
在Groovy 中有一个闭包的概念。闭包可以理解为就是 Java 中的匿名内部类。闭包支持类似lamda
形式的语法调用。如下:
- def square = { num ->
- num * num
- }
- square 8
如果只有一个参数,我们甚至可以省略这个参数,默认使用it
作为参数,最后代码是这样的:
- Closure square = {
- it * it
- }
- square 16
理解闭包的语法后,我们会发现,其实在我们之前的配置文件里,android
,dependencies
这些后面紧跟的代码块,都是一个闭包而已。
了解完 groovy 的基本语法后,我们来看看 gradle 里面的代码就好理解多了。
apply plugin: 'com.android.application'
这段代码其实就是调用了project
对象的apply
方法,传入了一个以plugin
为key的map。完整写出来就是这样的:project.apply([plugin: 'com.android.application'])
实际调用的时候会传入一个DependencyHandler
的闭包,代码如下:
运行该 task
./gradlew hello
注意:我们前面说过,gradle的生命周期分三步,初始化,配置和执行。上面的代码在配置过程就已经执行了,所以,打印出的字符串发生在该任务执行之前,如果要在执行阶段才执行任务中的代码应该如下设置:
doFirst()
和doLast()
方法。
打印出来是这样的:
must RunAfter
和dependsOn
。比如:task task1 <<{ printfln="" 'task1'="" }="" task="" task2="" <<{="" 'task2'="" task2.mustRunAfter="" task1<="" code=""/>
task task1 <<{ printfln="" 'task1'="" }="" task="" task2="" <<{="" 'task2'="" task2.dependsOn="" task1<="" code=""/>
他们的区别是,运行的的时候前者必须要都按顺序加入gradlew task2 task1
执行才可以顺利执行,否则单独执行每个任务,后者只需要执行gradlew task2
即可同时执行两个任务。
我们可以通过两个例子来实践task。
这里直接将 store 的密码明文写在这里对于产品的安全性来说不太好,特别是如果该源码开源,别人就可以用你的 id 去发布app。对于这种情况,我们需要构建一个动态加载任务,在编译release 源码的时候从本地文件(未加入git)获取keystore 信息,如下:
你还可以设置一个保险措施,万一我们的没有找到对应的文件需要用户从控制台输入密码
最后设置最终值
然后设置release 任务依赖于我们刚刚设置的任务
最后编译出来的apk 名字类似 app-debug-1.0.apk
。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。