Android studiooh-my-androidAndroid知识

[转载]Gradle 完整指南(Android)

2016-11-20  本文已影响257人  Liuuuuuuzi

前言

为什么需要学Gradle?

Gradle 是 Android 现在主流的编译工具,虽然在Gradle 出现之前和之后都有对应更快的编译工具出现,但是 Gradle 的优势就在于它是亲儿子,Gradle 确实比较慢,这和它的编译过程有关,但是现在的Gradle 编译速度已经有了成倍提高。除此之外,相对其他编译工具,最重要的,他和 Android Studio 的关系非常紧密,可以说对于一些简单的程序我们几乎不需要任何代码上的配置只使用 Android Studio 就可以完成编译和运行。

但是对于一些比较复杂的,特别是多人团队合作的项目我们会需要一些个性化的配置来提高我们的开发效率。比如我们要自定义编译出的apk包的名字、对于一些特殊产品我们可能会要用同一个项目编译出免费版付费版 的apk。这些高级的功能都需要我们对配置代码进行自定义地修改。

最近伴随着 Android Studio2.0的发布, Gradle 也进行了一次非常大的升级,叫Instant Run.它的编译速度网上有人用逆天两个字来形容。当我们第一次点击run、debug按钮的时候,它运行时间和我们往常一样。但是接下去的时间里,你每次修改代码后点击run、debug按钮,对应的改变将迅速的部署到你正在运行的程序上,传说速度快到你都来不及把注意力集中到手机屏幕上,它就已经做好相应的更改。但是刚出来的似乎对一些项目的兼容性不太好,现在升级后不知道怎么样。

为什么要了解命令行编译?

在很多情况下我们都是使用的 Android Studio 来build、debug项目。Android Studio 能满足我们开发的大多数需求,但是某些情况下命令行能够让我们编译的效率更高,过程更明朗,一些高级的配置也需要熟悉命令行才能够使用,比如在服务器编译,某些项目初始化的时候如果直接交给Android Studio ,它会一直Loading,你都不知道它在干嘛,但是用命令行你就知道它卡在了哪个环节,你只需要修改某些代码,马上就能够编译过了。

了解 Gradle 之后我们可以做什么?

we can do everything what we want.

Gralde Overview

History

我们知道,Android 的编译过程非常复杂:

图01

我们需要一种工具帮我们更快更方便更简洁地完成 Android 程序的编译。现在结合Android Studio 我们一般使用的工具都是Gradle, 在 Gradle 出现以前Android 也有对应的编译工具叫 Ant,在Gradle 出现之后,也有新的编译工具出现,就是FaceBook 的Buck工具。这些编译工具在出现的时候几乎都比 Gradle 要快,Gradle 之所以慢是跟它的编译周期有很大关系。

Gradle 的编译周期

在解析 Gradle 的编译过程之前我们需要理解在 Gradle 中非常重要的两个对象。Project和Task。

每个项目的编译至少有一个 Project,一个 build.gradle就代表一个project,每个project里面包含了多个task,task 里面又包含很多action,action是一个代码块,里面包含了需要被执行的代码。

在编译过程中, Gradle 会根据 build相关文件,聚合所有的projecttask,执行task 中的 action。因为build.gradle文件中的task非常多,先执行哪个后执行那个需要一种逻辑来保证。这种逻辑就是依赖逻辑,几乎所有的Task 都需要依赖其他 task 来执行,没有被依赖的task 会首先被执行。所以到最后所有的 Task 会构成一个 有向无环图(DAG Directed Acyclic Graph)的数据结构。

编译过程分为三个阶段:

刚刚我们提到Gradle 编译的时候的一些相关文件,下面我们挨个解析一下这些文件。

Gradle Files

对于一个gradle 项目,最基础的文件配置如下:

图02

一个项目有一个setting.grade、包括一个顶层的 build.grade文件、每个Module 都有自己的一个build.grade文件。

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.2.3'
    }
}

allprojects{
    repositories{
        jcenter()
    }
}
图03

Gradle Wrapper

Gradle 不断的在发展,新的版本难免会对以往的项目有一些向后兼容性的问题,这个时候,gradle wrapper就应运而生了。

gradlw wrapper 包含一些脚本文件和针对不同系统下面的运行文件。wrapper 有版本区分,但是并不需要你手动去下载,当你运行脚本的时候,如果本地没有会自动下载对应版本文件。

在不同操作系统下面执行的脚本不同,在 Mac 系统下执行

./gradlew ...

在windows 下执行

gradle.bat

进行编译。

如果你是直接从eclipse 中的项目转换过来的,程序并不会自动创建wrapper脚本,我们需要手动创建。在命令行输入以下命令即可

gradle wrapper --gradle-version 2.4
图04

wrapper 就是我们使用命令行编译的开始。下面我们看看 wrapper 有什么样的作用。

Gradle basics

Gradle 会根据build 文件的配置生成不同的task,我们可以直接单独执行每一个task。通过

./gradlew tasks

列出所有task。如果通过同时还想列出每个task 对应依赖的其他task,可以使用

./gradlew tasks -all

其实每当我们在Android Studio点击 build,rebuild,clean菜单的时候,执行的就是一些gradle task.

Android tasks

有四个基本的 task, Android 继承他们分别进行了自己的实现:

这些都是基本的命令,在实际项目中会根据不同的配置,会对这些task 设置不同的依赖。比如 默认的 assmeble 会依赖 assembleDebug 和assembleRelease,如果直接执行assmeble,最后会编译debug,和release 的所有版本出来。如果我们只需要编译debug 版本,我们可以运行assembleDebug。

除此之外还有一些常用的新增的其他命令,比如 install命令,会将编译后的apk 安装到连接的设备。

我们运行的许多命令除了会输出到命令行,还会在build文件夹下生产一份运行报告。比如check命令会生成lint-results.html.在build/outputs中。

Configuration

BuildConfig

这个类相信大家都不会陌生,我们最常用的用法就是通过BuildConfig.DEBUG来判断当前的版本是否是debug版本,如果是就会输出一些只有在 debug 环境下才会执行的操作。 这个类就是由gradle 根据 配置文件生成的。为什么gradle 可以直接生成一个Java 字节码类,这就得益于我们的 gradle 的编写语言是Groovy, Groovy 是一种 JVM 语言,JVM 语言的特征就是,虽然编写的语法不一样,但是他们最终都会编程 JVM 字节码文件。同是JVM 语言的还有 Scala,Kotlin 等等。

这个功能非常强大,我们可以通过在这里设置一些key-value对,这些key-value 对在不同编译类型的 apk 下的值不同,比如我们可以为debug 和release 两种环境定义不同的服务器。比如:

图05

除此之外,我们还可以为不同的编译类型的设置不同的资源文件,比如:

图06

Repositories

Repositories 就是代码仓库,这个相信大家都知道,我们平时的添加的一些 dependency 就是从这里下载的,Gradle 支持三种类型的仓库:Maven,Ivy和一些静态文件或者文件夹。在编译的执行阶段,gradle 将会从仓库中取出对应需要的依赖文件,当然,gradle 本地也会有自己的缓存,不会每次都去取这些依赖。

gradle 支持多种 Maven 仓库,一般我们就是用共有的jCenter就可以了。
有一些项目,可能是一些公司私有的仓库中的,这时候我们需要手动加入仓库连接:

图07

如果仓库有密码,也可以同时传入用户名和密码

图08

我们也可以使用相对路径配置本地仓库,我们可以通过配置项目中存在的静态文件夹作为本地仓库:

图09

Dependencies

我们在引用库的时候,每个库名称包含三个元素:组名:库名称:版本号,如下:

图10

如果我们要保证我们依赖的库始终处于最新状态,我们可以通过添加通配符的方式,比如:

图11

但是我们一般不要这么做,这样做除了每次编译都要去做网络请求查看是否有新版本导致编译过慢外,最大的弊病在于我们使用过的版本很很困难是测试版,性能得不到保证,所以,在我们引用库的时候一定要指名依赖版本。

Local dependencies
File dependencies

通过files()方法可以添加文件依赖,如果有很多jar文件,我们也可以通过fileTree()方法添加一个文件夹,除此之外,我们还可以通过通配符的方式添加,如下:

图12
Native libraries

配置本地 .so库。在配置文件中做如下配置,然后在对应位置建立文件夹,加入对应平台的.so文件。

图13

文件结构如下:

图14
Library projects

如果我们要写一个library项目让其他的项目引用,我们的bubild.gradle的plugin 就不能是andrid plugin了,需要引用如下plugin

apply plugin: 'com.android.library'

引用的时候在setting文件中include即可。

如果我们不方便直接引用项目,需要通过文件的形式引用,我们也可以将项目打包成aar文件,注意,这种情况下,我们在项目下面新建arrs文件夹,并在build.gradle 文件中配置 仓库:

图15

当需要引用里面的某个项目时,通过如下方式引用:

图16

Build Variants

在开发中我们可能会有这样的需求:

Build Type

android 默认的带有Debug和Release两种编译类型。比如我们现在有一个新的statging的编译类型

图17
Source sets

每当创建一个新的build type 的时候,gradle 默认都会创建一个新的source set。我们可以建立与main文件夹同级的文件夹,根据编译类型的不同我们可以选择对某些源码直接进行替换。

图18

除了代码可以替换,我们的资源文件也可以替换

除此之外,不同编译类型的项目,我们的依赖都可以不同,比如,如果我需要在staging和debug两个版本中使用不同的log框架,我们这样配置:

图19
Product flavors

前面我们都是针对同一份源码编译同一个程序的不同类型,如果我们需要针对同一份源码编译不同的程序(包名也不同),比如 免费版和收费版。我们就需要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

图20

根据我们的配置,再次查看我们的task,发现多了这些task:

图21

Resource merge priority

在Build Type中定义的资源优先级最大,在Library 中定义的资源优先级最低。

Signing configurations

如果我们打包市场版的时候,我们需要输入我们的keystore数据。如果是debug 版本,系统默认会帮我们配置这些信息。这些信息在gradle 中都配置在signingConfigs中。

图22

配置之后我们需要在build type中直接使用

图23

Optimize

Speeding up multimodule builds

可以通过以下方式加快gradle 的编译:

org.gradle.parallel=true
org.gradle.daemon=true
org.gradle.jvmargs=-Xms256m -Xmx1024m
Reducing apk file

在编译的时候,我们可能会有很多资源并没有用到,此时就可以通过shrinkResources来优化我们的资源文件,除去那些不必要的资源。

图24

如果我们需要查看该命令帮我们减少了多少无用的资源,我们也可以通过运行shrinkReleaseResources命令来查看log.

某些情况下,一些资源是需要通过动态加载的方式载入的,这时候我也需要像 Progard 一样对我们的资源进行keep操作。方法就是在res/raw/下建立一个keep.xml文件,通过如下方式 keep 资源:

图25
Manual shrinking

对一些特殊的文件或者文件夹,比如 国际化的资源文件、屏幕适配资源,如果我们已经确定了某种型号,而不需要重新适配,我们可以直接去掉不可能会被适配的资源。这在为厂商适配机型定制app的时候是很用的。做法如下:
比如我们可能有非常多的国际化的资源,如果我们应用场景只用到了English,Danish,Dutch的资源,我们可以直接指定我们的resConfig:

图26

对于尺寸文件我们也可以这样做

图27
Profiling

当我们执行所有task的时候我们都可以通过添加--profile参数生成一份执行报告在reports/profile中。示例如下:

图28

我们可以通过这份报告看出哪个项目耗费的时间最多,哪个环节耗费的时间最多。

Practice

在开发的过程中,我们可能会遇到很多情况需要我们能够自己定义task,在自定义task 之前,我们先简单看看groovy 的语法。

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!'
    }
}

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']

闭包

在Groovy 中有一个闭包的概念。闭包可以理解为就是 Java 中的匿名内部类。闭包支持类似lamda形式的语法调用。如下:

def square = { num ->
    num * num
}
square 8

如果只有一个参数,我们甚至可以省略这个参数,默认使用it作为参数,最后代码是这样的:

Closure square = {
    it * it
}
square 16

理解闭包的语法后,我们会发现,其实在我们之前的配置文件里,android,dependencies这些后面紧跟的代码块,都是一个闭包而已。

Groovy in Gradle

了解完 groovy 的基本语法后,我们来看看 gradle 里面的代码就好理解多了。

apply plugin: 'com.android.application'

这段代码其实就是调用了project对象的apply方法,传入了一个以plugin为key的map。完整写出来就是这样的:

project.apply([plugin: 'com.android.application'])
图29

实际调用的时候会传入一个DependencyHandler的闭包,代码如下:

图30

Task

图31

运行该 task

./gradlew hello

注意:我们前面说过,gradle的生命周期分三步,初始化,配置和执行。上面的代码在配置过程就已经执行了,所以,打印出的字符串发生在该任务执行之前,如果要在执行阶段才执行任务中的代码应该如下设置:

图32

添加Action:前面我们说过task 包含系列的action,当task 被执行的时候,所有的action 都会被依次执行。如果我们要加入自己的action,我们可以通过复写doFirst()doLast()方法。

图33

打印出来是这样的:

图34

Task 依赖:前面我们也说过,task 之间的关系就是依赖关系,关于Task 的依赖有两种,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即可同时执行两个任务。

Practice

我们可以通过两个例子来实践task。

keystore 保护

图35

这里直接将 store 的密码明文写在这里对于产品的安全性来说不太好,特别是如果该源码开源,别人就可以用你的 id 去发布app。对于这种情况,我们需要构建一个动态加载任务,在编译release 源码的时候从本地文件(未加入git)获取keystore 信息,如下:

图36

你还可以设置一个保险措施,万一我们的没有找到对应的文件需要用户从控制台输入密码

图37

最后设置最终值

然后设置release 任务依赖于我们刚刚设置的任务

图38

通过 hook Android 编译插件 重命名 apk

图39
图40

最后编译出来的apk 名字类似 app-debug-1.0.apk

总结

按照习惯还是要写一个总结,本来也是想写一个关于Gradle在Android中的使用,网上查阅相关资料时看到了这篇文章,总结的相当完整,先直接分享出来吧,接下来还是会写关于Gradle的使用, 到时候会把各个部分分开来详细说说。

上一篇下一篇

猜你喜欢

热点阅读