Gradle

一、概念

APK的构建过程

APK的构建过程

APK的构建过程比较繁琐,主要分为以下几步:
1.通过AAPT(Android Asset Packaging Tool)打包res资源文件,比如AndroidManifest.xml、xml布局文件等,并将这些xml文件编译为二进制,其中assets和raw文件夹的文件不会被编译为二进制,最终会生成R.java和resources.arsc文件。
2.AIDL工具会将所有的aidl接口转化为对应的Java接口。
3.所有的Java代码,包括R.java和Java接口都会被Java编译器编译成.class文件。
4.Dex工具会将上一步生成的.class文件、第三方库和其他.class文件编译成.dex文件。
5.上一步编译生成的.dex文件、编译过的资源、无需编译的资源(如图片等)会被ApkBuilder工具打包成APK文件。
6.使用Debug Keystore或者Release Keystore对上一步生成的APK文件进行签名。
7.如果是对APK正式签名,还需要使用zipalign工具对APK进行对齐操作,这样应用运行时会减少内存的开销。

Gradle

Gradle是一个自动化构建工具,通过组织一系列Task来最终完成自动化构建,所以Task是Gradle里最重要的概念。我们以生成一个可用的apk为例,整个过程要经过资源的处理、javac编译、dex打包、apk打包、签名等步骤,每个步骤就对应到Gradle里的一个Task。Gradle可以类比作一条流水线,Task可以比作流水线上的机器人,每个机器人负责不同的事情,最终生成完整的构建产物。

Gradle脚本使用了Groovy或者Kotlin DSL(Domain Specific Language),不过目前还是Groovy居多。对Gradle脚本来说,DSL实现了简洁的定义,又有充分的语言逻辑,以android {}为例,这本身是一个函数调用,参数是一个闭包,但是这种定义方式明显要简洁很多。

Gradle基于Groovy编写,而Groovy是基于JVM的语言,本质上是面向对象的语言。面向对象语言的特点就是一切皆对象,所以在Gradle里,.gradle脚本的本质就是类的定义,一些配置项的本质都是方法调用,参数是后面的{}闭包。比如build.gradle对应Project类,buildScript对应Project.buildScript方法。

Android Gradle插件

Gradle跟Android Studio其实没有关系,Google在推出Android Studio的时候选中了Gradle作为构建工具,为了支持Gradle能在Android Studio上使用,Google做了个Android Studio的插件,叫做Android Gradle插件,所以我们能在Android Studio上使用Gradle完全是因为这个插件的原因。

在项目根目录有个build.gradle文件,里面有这么一句代码:

classpath 'com.android.tools.build:gradle:3.2.1'

这个就是依赖Android Gradle插件的代码,后面的版本号代表的是Android Gradle插件的版本,而不是Gradle的版本,这个是Google定的,跟Gradle官方没关系。

Gradle Wrapper

Gradle Wrapper意为Gradle的包装,假设我们本地有多个项目,一个是比较老的项目用着Gradle 1.0的版本,一个是比较新的项目用了Gradle 2.0的版本,但是两个项目肯定都想要同时运行的,如果只装了Gradle 1.0的话那肯定不行,所以为了解决这个问题,Google推出了Gradle Wrapper的概念,就是在每个项目都配置了一个指定版本的Gradle,这样就可以支持用不同的Gradle版本来构建项目。

Wrapper在Window下是一个批处理脚本,在Linux下是一个shell脚本,当使用wrapper启动Gradle的时候,wrapper会检查Gradle有没有被下载关联,如果没有将会从配置的地址进行下载并运行构建。

查看当前项目所用Gradle版本的方法:使用终端切换到项目根目录,输入:gradlew -v。

Project Task Action

每次构建(build)至少由一个Project构成,一个Project由一到多个Task构成。项目结构中的每个build.gradle文件代表一个Project,在这编译脚本文件中可以定义一系列的Task,Task是由一组被顺序执行的Action对象构成。

点击Android Studio右侧的Gradle按钮,会打开一个面板,可以查看Tasks。

二、Gradle构建的生命周期

初始化阶段

Project实例会在该阶段被创建。如果一个项目有多个模块,并且每一个模块都有其对应的build.gradle文件,那么就会创建多个Project实例。

配置阶段

在该阶段,构建脚本会被执行,并为每个Project实例创建和配置Task。

执行阶段

在该阶段,Gradle将决定哪个Task会被执行。哪些Task被执行取决于开始该次构建的参数配置和该Gradle文件的当前目录。

三、Android Gradle插件主要流程

项目添加Android Gradle插件依赖:

dependencies {
    classpath 'com.android.tools.build:gradle:3.2.1' //Android Gradle插件提供了构建和测试应用所需要的一切
}

准备工作

1.检查插件版本。
2.检查module是否重名。
3.初始化插件信息。

配置项目

1.检查Gradle版本是否匹配。
2.创建AndroidBuilder和DataBindingBuilder。
3.引入java plugin和jacoco plugin。
4.设置构建完成以后的缓存清理工作。

配置Extension

1.创建AppExtension,也就是build.gradle里用到的android {}dsl。
2.建依赖管理、ndk管理、任务管理、variant管理。
3.注册新增配置的回调函数,包括signingConfig、buildType、productFlavor。
4.创建默认的debug签名,创建debug和release两个buildType。

创建不依赖flavor的Task

主要创建Task:
uninstallAll,deviceCheck,connectedCheck,preBuild,extractProguardFiles,sourceSets,assembleAndroidTest,compileLint,lint,lintChecks,cleanBuildCacheresolveConfigAttr,consumeConfigAttr。
这些Task都是不需要依赖flavor数据的公共Task。

创建构建Task

1.populateVariantDataList
在方法里,会先根据flavor和dimension创建对应的组合,存放在flavorComboList里,之后调用createVariantDataForProductFlavors创建对应的VariantData。VariantData里保存了很多Task,下一步就要创建这些Task。
2.createTasksForVariantData
创建完variant数据,就要给每个variantData创建对应的Task,对应的Task有assembleXXXTask,prebuildXXX,generateXXXSource,generateXXXResources,generateXXXAssets,processXXXManifest等。

四、Groovy

概念

Groovy是Apache旗下的一种基于JVM的面向对象编程语言,既可以用于面向对象编程,也可以用作纯粹的脚本语言。Groovy与Java可以很好的互相调用并结合编程,比如在写Groovy的时候忘记了语法可以直接按Java的语法继续写,也可以在Java中调用Groovy脚本。比起Java,Groovy语法更加的灵活和简洁,可以用更少的代码来实现Java实现的同样功能。

编写与调试

Groovy的代码可以在Android Studio中进行编写和调试。具体的操作步骤是:在一个目录中新建build.gradle文件,在build.gradle中新建一个Task,在Task中编写Groovy代码,用命令行进入这个build.gradle文件所在的目录,运行gradlew Task名称 等命令行对代码进行调试。

变量

Groovy中用def关键字来定义变量,可以不指定变量的类型,默认访问修饰符是public。

def a = 1;
def int b = 1;
def c = "hello world";

方法

方法使用返回类型或def关键字定义,方法可以接收任意数量的参数,这些参数可以不申明类型,如果不提供可见性修饰符,则该方法为public。

用def关键字定义方法:
//build.gradle

task method <<{
    add (1,2)
    minus 1,2 //方法调用时括号可以省略
}
def add(int a,int b) {
    println a+b //语句后面的分号可以省略
}
def minus(a,b) { //方法定义时参数类型可以省略
    println a-b
}

//运行
在Terminal中输入:gradlew -q method

//输出
3
-1

如果指定了方法返回类型,可以不需要def关键字来定义方法:
//build.gradle

task method <<{
    def number=minus 1,2
    println number
}
int minus(a,b) {
    return a-b
}

//运行
在Terminal中输入:gradlew -q method

//输出
-1

如果不使用return,方法的返回值为最后一行代码的执行结果:
//build.gradle

task method <<{
    def number=minus 1,2
    println number
}
int minus(a,b) {
    a-b //return可以省略
}

//运行
在Terminal中输入:gradlew -q method

//输出
-1

Groovy中有很多省略的地方:

  • 语句后面的分号可以省略。
  • 方法调用时括号可以省略。
  • 方法定义时参数类型可以省略。
  • return可以省略。

Groovy类非常类似于Java类,Groovy类与Java类有以下区别:

  • 默认类的修饰符为public。
  • 没有可见性修饰符的字段会自动生成对应的setter和getter方法。
  • 类不需要与它的源文件有相同的名称,但还是建议采用相同的名称。

//build.gradle

task method <<{
    def p = new Person()
    p.increaseAge 5
    println p.age
}
class Person {
    String name
    Integer age =10
    def increaseAge(Integer years) {
        this.age += years
    }
}

//运行
在Terminal中输入:gradlew -q method

//输出
15

语句

断言

Groovy断言和Java断言不同,它一直处于开启状态,是进行单元测试的首选方式。当断言的条件为false时,程序会抛出异常,不再执行下面的代码,从输出可以很清晰的看到发生错误的地方。
//build.gradle

task method <<{
    assert 1+2 == 6
}

//运行
在Terminal中输入:gradlew -q method

//输出
Execution failed for task ':app:method'.
> assert 1+2 == 6
          |  |
          3  false

for循环

Groovy支持Java的for(int i=0;i<N;i++)和for(int i :array)形式的循环语句,另外还支持for in loop形式,支持遍历范围、列表、Map、数组和字符串等多种类型。

//遍历范围
def x = 0
for ( i in 0..3 ) {
    x += i
}
assert x == 6


//遍历列表
def x = 0
for ( i in [0, 1, 2, 3] ) {
    x += i
}
assert x == 6


//遍历Map中的值
def map = ['a':1, 'b':2, 'c':3]
def x = 0
for ( v in map.values() ) {
    x += v
}
assert x == 6

switch语句

Groovy中的Switch语句不仅兼容Java代码,还可以处理更多类型的case表达式。case表达式可以是字符串、列表、范围、Integer等

task method <<{
    def x = 16
    def result = ""

    switch ( x ) {
        case "ok":
            result = "found ok"
        case [1, 2, 4, 'list']:
            result = "list"
            break
        case 10..19:
            result = "range"
            break
        case Integer:
            result = "integer"
            break
        default:
            result = "default"
    }
    println result
    assert result == "range"
}

数据类型

Groovy中的数据类型主要有以下几种:

  • Java中的基本数据类型
  • Groovy中的容器类
  • 闭包

字符串

在Groovy种有两种字符串类型,普通字符串String(java.lang.String)和插值字符串GString(groovy.lang.GString)。

单引号字符串:
在Groovy中单引号字符串和双引号字符串都可以定义一个字符串常量,只不过单引号字符串不支持插值。
//build.gradle

def name = 'Android高级工程师'

task method <<{
    println name
}

//运行
在Terminal中输入:gradlew -q method

//输出
Android高级工程师

双引号字符串:
双引号字符串支持插值,插值指的是替换字符串中的占位符,占位符表达式为{}或者以为前缀。
//build.gradle

def name = 'Android高级工程师'

task method <<{
    println "hello ${name}"
    println "hello $name"
}

//运行
在Terminal中输入:gradlew -q method

//输出
hello Android高级工程师
hello Android高级工程师

三引号字符串:
三引号字符串可以保留文本的换行和缩进格式,不支持插值。
//build.gradle

def name = '''Android高级工程师
    1Tab
        2Tab'''

task method <<{
    println name
}

//运行
在Terminal中输入:gradlew -q method

//输出
Android高级工程师
    1Tab
        2Tab

GString:
String是不可变的,GString却是可变的,GString和String即使有相同的字面量,它们的hashCodes的值也可能不同,因此应该避免使用使用GString作为Map的key。
//build.gradle

task method <<{
    println "one: ${1}".hashCode() //当双引号字符串中包含插值表达式时,字符串类型为GString
    println "one: 1".hashCode() //String类型
}

//运行
在Terminal中输入:gradlew -q method

//输出
-1012478518
-1012478555

List

Groovy没有定义自己的集合类,它在Java集合类的基础上进行了增强和简化。Groovy的List对应Java中的List接口,默认的实现类为Java中的ArrayList。
//build.gradle

task method <<{
    def number = [1, 2, 3]
    assert number instanceof List
    def linkedList = [1, 2, 3] as LinkedList //使用as操作符来显式指定List的实现类为java.util.LinkedList
    assert linkedList instanceof java.util.LinkedList
}


task method <<{
    def number  = [1, 2, 3, 4]
    println number [1] //1表示列表索引为1的元素(索引从0开始算起)
    println number [-1] //-1表示列表末尾的第一个元素

    number << 5 //<<运算符表示在列表末尾追加一个元素
    println number [4]
    println number [-1]
}

//运行
在Terminal中输入:gradlew -q method

//输出
2
4
5
5

Map

创建Map同样使用[],需要同时指定键和值,默认的实现类为java.util.LinkedHashMap。
//build.gradle

task method <<{
    def name = [one: 'Android', two: 'Java', three: 'Other']
    println name['one']
    println name.two
}

//运行
在Terminal中输入:gradlew -q method

//输出
Android
Java


task method <<{
    def key = 'name'
    def person = [key: 'Android'] //键值是key这个字符串,而不是key变量的值name
    println person.containsKey('key')
    person = [(key): 'Java'] //键值是key变量的值name,而不是key这个字符串
    println person.containsKey('name')
}

//运行
在Terminal中输入:gradlew -q method

//输出
true
true

闭包

Groovy中的闭包是一个开放的、匿名的、可以接受参数和返回值的代码块。

定义闭包:

{ [closureParameters -> ] statements }

闭包分为两个部分,分别是参数列表部分[closureParameters -> ]和语句部分 statements 。参数列表部分是可选的,如果闭包只有一个参数,参数名是可选的,Groovy会隐式指定it作为参数名。

{ String a, String b ->                                
    println "${a} is a ${b}"
}


{ println it } //隐式指定it作为参数名


{ it -> println it }   //it是一个显示参数 

闭包赋值:
闭包是groovy.lang.Cloush类的一个实例,这使得闭包可以赋值给变量或字段。

//将闭包赋值给一个变量
def println ={ it -> println it }     
assert println instanceof Closure


//将闭包赋值给Closure类型变量
Closure do= { println 'do!' }

调用闭包:
闭包既可以当做方法来调用,也可以显示调用call方法。
//build.gradle

task method <<{
    def code = { 123 }
    println code() //闭包当做方法调用
    println code.call() //显示调用call方法
}

//运行
在Terminal中输入:gradlew -q method

//输出
123
123


task method <<{
    def code = { int i -> i%2 }
    println code(3) //调用带参数的闭包
    println code.call(3) //调用带参数的闭包
}

//运行
在Terminal中输入:gradlew -q method

//输出
1
1

I/O 操作

Groovy的 I/O 操作要比Java的更为的简洁。

文件读取

//build.gradle

task method <<{
    def filePath = "D:/test.txt"
    def file = new File(filePath)
    file.eachLine {
        println it
    }
}

//运行
在Terminal中输入:gradlew -q method

//输出
hello
Android
Java


task method <<{
    def filePath = "D:/test.txt"
    def file = new File(filePath)
    println file.text
}

//运行
在Terminal中输入:gradlew -q method

//输出
hello
Android
Java

文件写入

//build.gradle

task method <<{
    def filePath = "D:/test.txt"
    def file = new File(filePath)
    file.withPrintWriter {
        it.println("hello2")
        it.println("Android2")
        it.println("Java2")
    }
}

//运行
在Terminal中输入:gradlew -q method

//输出(test.txt)
hello2
Android2
Java2

其它

asType

asType可以用于数据类型转换。

task method <<{
    String a = '123'
    int b = a as int
    def c = a.asType(Integer)
    assert b instanceof java.lang.Integer //true
    assert c instanceof java.lang.Integer //true
}

判断是否为真

task method <<{
    if (name != null && name.length > 0) { //true
    }
}


task method <<{
    if (name) {
    }
}

安全取值

在Java中,要安全获取某个对象的值可能需要大量的if语句来判空,例如:

if (school != null) {
    if (school.getStudent() != null) {
        if (school.getStudent().getName() != null) {
            System.out.println(school.getStudent().getName());
        }
    }
}

在Groovy中可以使用?.来安全的取值,例如:

println school?.student?.name

with操作符

对同一个对象的属性进行赋值时,可以这么做:

task method <<{
    Person p = new Person()
    p.name = "Android"
    p.age = 10
    println p.name + " " + p.age
}
class Person {
    String name
    Integer age
}

使用with操作符进行简化:

task method <<{
    Person p = new Person()
    p.with {
        name = "Android"
        age= 10
    }
    println p.name + " " + p.age
}
class Person {
    String name
    Integer age
}

五、Task基础

创建Task

方式1:doLast
//build.gradle

task hello {
    doLast {
        println 'Hello world!'
    }
}

//运行
在Terminal中输入:gradlew -q hello

//输出
Hello world!

方式2:操作符<<是doLast方法的快捷版本
//build.gradle

task hello << {
    println 'Hello world!'
}

//运行
在Terminal中输入:gradlew -q hello

//输出
Hello world!

方式3:用任务名称创建
//build.gradle

def Task hello=task(hello)
hello.doLast{
    println "hello world"
}

//运行
在Terminal中输入:gradlew -q hello

//输出
Hello world!

方式4:用任务名称与任务配置创建
//build.gradle

def Task hello=task(hello,group:BasePlugin.BUILD_GROUP)
hello.doLast{
    println "hello world"
}

//运行
在Terminal中输入:gradlew -q hello

//输出
Hello world!

方式5:用TaskContainer的create方法创建
//build.gradle

tasks.create(name: 'hello') << {
    println "hello world"
}

//运行
在Terminal中输入:gradlew -q hello

//输出
Hello world!

Task依赖

//build.gradle

task hello << {
    println 'Hello world!'
}
task go(dependsOn: hello) << {
    println "go for it"
}

//运行
在Terminal中输入:gradlew -q go

//输出
Hello world!
go for it

动态定义Task

//build.gradle

3.times {number ->
    task "task$number" << {
        println "task $number"
    }
}

//运行
在Terminal中输入:gradlew -q task2

//输出
task 2

times是Groovy在java.lang.Number中拓展的方法,是一个定时器。3.times中循环创建了三个新任务,隐式变量number的值为0,1,2,任务的名称由task加上number的值组成,达到了动态定义任务的目的。

Task的分组和描述

//build.gradle

task hello {
    group = 'build'
    description = 'hello world'
    doLast {
        println "任务分组: ${group}"
        println "任务描述: ${description}"
    }
}
task go(dependsOn: hello) << {
    println "go for it"
}

//运行
在Terminal中输入:gradlew -q go

//输出
任务分组: build
任务描述: hello world
go for it


def Task hello=task(hello)
hello.description ='hello world'
hello.group=BasePlugin.BUILD_GROUP
hello.doLast{
    println "任务分组: ${group}"
    println "任务描述: ${description}"
}
task go(dependsOn: hello) << {
    println "go for it"
}

//运行
在Terminal中输入:gradlew -q go

//输出
任务分组: build
任务描述: hello world
go for it

Task的一些重要方法

Task行为:
Task.doFirst
Task.doLast

Task依赖顺序:
Task.dependsOn
Task.mustRunAfter
Task.shouldRunAfter
Task.finalizedBy

Task的分组描述:
Task.group
Task.description

Task是否可用:
Task.enabled

Task输入输出:
Task.inputs
Task.outputs

Task是否执行:
Task.upToDateWhen

Gradle日志级别

配置如下:
无日志选项:LIFECYCLE及更高级别
-q或者--quiet:QUIET及更高级别
-i或者--info:INFO及更高级别
-d或者--debug:DEBUG及更高级别,这一般会输出所有日志

gradlew -q hello
gradlew --quiet hello

Gradle输出错误堆栈信息

在使用Gradle构建的时候,难免会有这样或者那样的问题导致构建失败,这时就需要根据日志分析解决问题。除了日志信息之外,Gradle还提供了堆栈信息的打印,能帮助我们很好地定位和分析问题。默认情况下,堆栈信息的输出是关闭的,需要我们通过命令行的堆栈信息开关打开它,这样在我们构建失败的时候,Gradle才会输出错误堆栈信息。

配置如下:
无选项:没有堆栈信息输出
-s 或者 --stacktrace:输出关键性的堆栈信息
-S 或者 --full-stacktrace:输出全部堆栈信息
一般推荐使用-s而不是-S,因为-S输出的堆栈太多太长,非常不好看,而-s反而比较精简,可以定位解决我们大部分的问题。

自己使用日志信息调试

在编写Gradle脚本的过程中,我们有时候需要输出一些日志,来验证我们的逻辑或者一些变量的值是否正确,这时候我们就可以使用Gradle提供的日志功能。

通常情况下我们都是使用print系列方法,把日志信息输出到标准的控制台输出流(它被Gradle定向为QUIET级别日志):

task method <<{
    println "输出日志进行验证"
}

除了print系列方法之外,也可以使用内置的logger(实际上是调用Project的getLogger()方法获取的Logger对象实例)更灵活地控制输出不同级别的日志信息。

task method <<{
    logger.quiet('quiet日志信息.')
    logger.error('error日志信息.')
    logger.warn('warn日志信息.')
    logger.lifecycle('lifecycle日志信息.')
    logger.info('info日志信息.')
    logger.debug('debug日志信息.')
}

Gradle命令行

gradlew -q 任务名称:运行一个指定的任务
gradlew -q tasks:获取所有的任务信息,默认情况下只会显示那些被分组的任务的名称和描述
gradlew -q 任务名称1 -x 任务名称2:运行任务1,排除任务2
gradlew -q help --task 任务名称:获取任务帮助信息
gradlew -q 任务名称1 -q 任务名称2:先运行任务1,再运行任务2

gradlew -q hello
gradlew -q tasks
gradlew -q go -x hello
gradlew -q help --task hello
gradlew -q helloWorld -q goForit

六、Gradle项目分析

settings.gradle

Settings API文档

settings.gradle是负责配置项目的脚本,对应Settings类,Gradle构建过程中,会根据settings.gradle生成Settings对象。

settings.gradle:

include ':app', ':app2'

rootproject/build.gradle

Project API文档

build.gradle负责整体项目的一些配置,对应的是Project类。Gradle构建过程中,会根据build.gradle生成Project对象(Project其实是一个接口,真正的实现类是DefaultProject),所以在build.gradle里写的DSL,其实都是Project接口的一些方法。

主要方法:

  • buildscript:配置脚本的classpath
  • allprojects:配置项目及其子项目
  • repositories:配置仓库地址,后面的依赖都会到这里的地址进行查找
  • dependencies:配置项目的依赖

build.gradle(project):

buildscript { //配置项目的classpath
    repositories { //项目的仓库地址,会按顺序依次查找
        maven {
            name 'AliYun Google Repository Proxy'
            url 'https://maven.aliyun.com/repository/google'
        }
        maven {
            name 'AliYun Jcenter Repository Proxy'
            url 'https://maven.aliyun.com/repository/jcenter'
        }
    }

    dependencies { //项目的依赖
        classpath 'com.android.tools.build:gradle:3.2.1'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects { //子项目的配置
    repositories {
        maven {
            name 'AliYun Google Repository Proxy'
            url 'https://maven.aliyun.com/repository/google'
        }
        maven {
            name 'AliYun Jcenter Repository Proxy'
            url 'https://maven.aliyun.com/repository/jcenter'
        }
    }
}

ext { //定制项目属性
    compileSdkVersion = 28 //可以在module的build.gradle进行引用:compileSdkVersion rootProject.ext.compileSdkVersion
}

module/build.gradle

build.gradle是子项目的配置,对应的也是Project类。子项目和根项目的配置是差不多的,不过在子项目里可以看到有一个明显的区别,就是引用了一个插件:apply plugin "com.android.application",后面的Android DSL就是application插件的extension。

主要方法:

  • compileSdkVersion:指定编译需要的sdk版本
  • defaultConfig:指定默认的属性,会运用到所有的variants上
  • buildTypes:一些编译属性可以在这里配置
  • productFlavor:配置项目的flavor

build.gradle(module):

//com.android.application 表示Android应用模块插件
//com.android.library 表示Android依赖模块插件
//插件用于扩展Gradle构建脚本的能力。在一个项目中应用一个插件,该项目就可以使用该插件预定义的一些属性和任务。
apply plugin: 'com.android.application' 

android { //配置Android Gradle插件需要的内容
    compileSdkVersion 28 //用来编译应用的Android API版本
    //buildToolsVersion '28.0.3'
    //buildToolsVersion是构建工具和编译器使用的版本号。构建工具包含命令行应用,如aapt、zipalign、dx和renderscript,这些都被用来在打包应用时生成各种中间产物。
    //Android Gradle插件的版本在3.x之后默认所有的项目都使用相同的buildToolsVersion,不需要用户单独指定项目buildToolsVersion
    defaultConfig { //配置应用的核心属性
        applicationId "com.tomorrow.architetest" 
        minSdkVersion 15
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        resConfigs 'en', 'zh-rCN' ,'es', 'hdpi' //过滤语言、过滤drawable文件夹资源
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
    buildTypes { //定义如何构建和打包不同构建类型的应用
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions { //指定Java版本
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }
    
    lintOptions { //link检测
        abortOnError false
    }
    
    //flavor相关配置
    flavorDimensions "size", "color"
    productFlavors {
        big {
            dimension "size"
        }
        small {
            dimension "size"
        }
        blue {
            dimension "color"
        }
        red {
            dimension "color"
        }
    }
}

//项目需要的依赖
dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar']) //jar包依赖
    implementation 'com.android.support:appcompat-v7:28.0.0' //远程仓库依赖
    implementation 'com.android.support:support-compat:28.0.0'
    implementation project(':module1') // 项目依赖
}

七、Android Task

在新建的Android项目中,其会包括Android tasks、build tasks、build setup tasks、help tasks、install tasks、verification tasks和其它tasks。

基础Task

Gradle的Android插件使用了Java基础插件,而Java基础插件又使用了基础插件,基础插件添加了Task的标准生命周期和一些共同约定的属性。基础插件定义了assemble和clean tasks,Java插件定义了check和build tasks。
这些Task的约定如下:

  • assemble:集合项目的输出。
  • clean:清理项目的输出。
  • check:运行所有检查,通常是单元测试和集成测试。
  • build:同时运行assemble和check。

Android Task

Android插件扩展了基础Task,并实现了它们的行为:

  • assemble:为每个构建版本创建一个APK。
  • clean:删除所有的构建内容,例如APK文件。
  • check:运行Link检查,如果Link发现一个问题,则可终止构建。
  • build:同时运行assemble和check。

assemble Task默认依赖于assembleDebug和assembleRelease,如果添加了更多的构建类型,那么就会有更多的Task。这意味着,assemble将会触发每一个构建类型,并进行一次构建操作。

除了扩展这些Task之外,Android Gradle插件还添加了一些新的Task:

  • connectedCheck:在连接设备或模拟器上运行测试。
  • deviceCheck:一个占位任务,专为其他插件在远端设备上运行测试。
  • installDebug和installRelease:在连接设备或模拟器上安装特定版本。
  • 所有的install tasks都会有uninstall tasks。

八、依赖

依赖配置

在Gradle 3.4里引入了新的依赖配置。

例子:

  • 项目里有三个模块:app、module1、module2
  • 模块app中有一个类ModuleApi
  • 模块module1中有一个类Module1Api
  • 模块module2中有一个类Module2Api
  • app依赖module1,module1依赖module2

新配置:implementation
弃用配置:compile
作用:api只会暴露给直接依赖的模块,大多数应用和测试模块都应使用此配置。
例子:当app使用implementation依赖module1、module1使用implementation依赖module2时,在app中无法引用到Module2Api类。


dependencies-implementation

新配置:api
弃用配置:compile
作用:api会暴露给间接依赖的模块。
例子:当app使用implementation依赖module1、module1使用api依赖module2时,在app中可以引用到Module2Api类。


dependencies-api

新配置:compileOnly
弃用配置:provided
作用:只在编译期间依赖模块,打包以后运行时不会依赖,可以用来解决一些库冲突的问题。
例子:当app使用implementation依赖module1、module1使用compileOnly依赖module2时,
在编译阶段,app无法引用到Module2Api类,module1可以引用到Module2Api类;
在运行阶段,module1无法引用到Module2Api类。


-dependencies-compileOnly

新配置:runtimeOnly
弃用配置:apk
作用:只在运行时依赖模块,编译时不依赖。
例子:当app使用implementation依赖module1、module1使用runtimeOnly依赖module2时,
在编译阶段,app无法引用到Module2Api类,module1无法引用到Module2Api类;
在运行阶段,module1可以引用到Module2Api类。


dependencies-runtimeOnly

依赖管理

1.依赖jcenter包
每个库名称包含三个元素:组名:库名称:版本号。

implementation 'com.android.support:appcompat-v7:25.0.0'

2.依赖本地module

implementation project(':YibaAnalytics')

3.依赖jar包
把jar包放在libs目录下,在build.gradle中添加依赖。

dependencies {
   implementation files('libs/YibaAnalytics5.jar')
}


dependencies {
   compile fileTree(dir: 'libs', include: ['*.jar'])
}

4.依赖aar包
把aar包放到libs目录下,在build.gradle中添加依赖。

repositories {
    flatDir {
        dirs 'libs'
    }
}
 
dependencies {
    compile(name:'YibaAnalytics-release', ext:'aar')
}

5.自定义依赖包目录
当aar包需要被多module依赖时,我们就不能把aar包放在单一的module中,我们可以在项目的根目录创建一个目录,比如叫aar目录,然后把aar包放进去。
在项目的根目录的build.gradle的allprojects标签下的repositories添加:

flatDir {
    dirs '../aar'
}

然后就可以添加依赖了:

compile(name:'YibaAnalytics-release', ext:'aar')

6.native包(so包)
用c或者c++写的library叫做so包,Android插件默认情况下支持native包,需要把.so文件放在jniLibs目录中,jniLibs目录应该和java目录在同一级。

九、创建构建Variant

当开发一个应用时,通常会有几个不同的版本。

构建类型

在Android Gradle插件中,构建类型通常被用来定义如何构建一个应用或依赖库。

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.tomorrow.architetest"
        minSdkVersion 15
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

当默认的设置不够用时,我们可以很容易地创建自定义构建类型。

buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
    staging.initWith(buildTypes.debug) //initWith创建一个新的构建类型,并且复制一个已经存在的构建类型的所有属性到新的构建类型中
    staging {
        applicationIdSuffix ".staging" //给applicationId添加后缀
        versionNameSuffix "-staging" //给versionName添加后缀
        debuggable false
    }
}

存在三个构建类型:debug、release、staging。

project flavor

在Android Gradle插件3.x之后,每个flavor必须对应一个dimension,可以理解为flavor的分组,然后不同dimension里的flavor两两组合形成一个variant。

flavorDimensions "size", "color"

productFlavors {
    big {
        dimension "size"
    }
    small {
        dimension "size"
    }
    blue {
        dimension "color"
    }
    red {
        dimension "color"
    }
}

那么生成的variant对应的就是bigBlue,bigRed,smallBlue,smallRed。

构建variant

构建variant是构建类型和product flavor结合的结果。不论什么时候创建一个构建类型或product flavor,新的variant都会被创建。

例如:

  • bigBlueDebug
  • bigRedDebug
  • smallBlueDebug
  • smallRedDebug
  • bigBlueRelease
  • bigRedRelease
  • smallBlueRelease
  • smallRedRelease

Task

Android Gradle插件会为每一个构建variant创建任务。

例如:

  • assembleBigBlueDebug
  • assembleBigRedDebug
  • assembleSmallBlueDebug
  • assembleSmallRedDebug
  • assembleBigBlueRelease
  • assembleBigRedRelease
  • assembleSmallBlueRelease
  • assembleSmallRedRelease

源集

构建variant,是一个构建类型和一个或多个product flavor的结合体,其可以有自己的源集文件夹。

例如:由debug构建类型、blue flavor和free flavor创建的variant可以有其自己的源集src/blueFreeDebug/java/。其可以通过使用sourceSets代码块来覆盖文件夹的位置。

SourceSet增加源码及资源目录

android {
    sourceSets {
        main {
            java.srcDir 'src/test1/java' //增加代码目录
            res.srcDirs 'src/test1/res' //增加资源目录
        }
    }
}

SourceSet定义AndroidManifest文件

android {
    def appDebug = false

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            appDebug = false
        }

        debug {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            appDebug = true
        }
    }

    sourceSets {
        main {
            if (appDebug) {
                manifest.srcFile 'src/test1/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            }
        }
    }
}

源集合并资源和manifest

Android Gradle插件在打包应用之前将main源集和构建类型源集合并在一起。此外,library项目也可以提供额外的资源,这些也需要被合并,这同样适用于manifest文件。

例如:在应用的debug variant中可能需要额外的Android权限来存储log文件,你并不想在main源集中声明该权限,因为这会吓跑潜在客户,你可以在debug构建类型的源集中额外添加一个manifest文件来声明额外的权限。

资源和manifest的优先顺序:
Build type > Flavor > Main > Dependencies
如果资源在flavor和main源集中都有声明,那么flavor中的资源将被赋予更高的优先级。在这种情况下,flavor源集中的资源将会被打包。在library项目中声明的资源通常具有最低优先级。

创建构建variant

android {
    buildTypes {
        debug {
            buildConfigField "String", "API_URL", "\"http://test.example.com/api\"" //buildConfigField自定义属性,可以在代码中进行引用:BuildConfig.API_URL
        }

        staging.initWith(android.buildTypes.debug)
        staging {
            buildConfigField "String", "API_URL", "\"http://staging.example.com/api\""
            applicationIdSuffix ".staging"
        }
    }
    
    productFlavors {
        red {
            applicationId "com.gradleforandroid.red"
            resValue "color", "flavor_color", "#ff0000"
        }

        blue {
            applicationId "com.gradleforandroid.blue"
            resValue "color", "flavor_color", "#0000ff"
        }
    }
}

variant过滤器

variant过滤器可以完全忽略某些variant。可以在App或library根目录下的build.gradle文件使用以下代码来过滤variants:

android.variantFilter{ variant ->
    if(variant.buildType.name.equals('release')) {
        variant.getFlavors().each() { flavor ->
            if(flavor.name.equals('blue')) {
                variant.setIgnore(true)
            }
        }
    }
}

签名配置

只有buildTypes:

android {
    signingConfigs { //实际应用中不要在构建配置文件中存储凭证,而是使用Gradle配置文件
        staging.initWith(signingConfigs.debug) //staging签名配置

        release { //release签名配置
            storeFile file("release.keystore")
            storePassword "secretpassword"
            keyAlias "gradleforandroid"
            keyPassword "secretpassword"
        }
    }

    buildTypes {
        release {
            signingConfigs signingConfigs.release
        }
    }
}

有productFlavors:

android {
    signingConfigs { //实际应用中不要在构建配置文件中存储凭证,而是使用Gradle配置文件
        staging.initWith(signingConfigs.debug) //staging签名配置

        release { //release签名配置
            storeFile file("release.keystore")
            storePassword "secretpassword"
            keyAlias "gradleforandroid"
            keyPassword "secretpassword"
        }
    }

    buildTypes {
        release {
            productFlavors.red.signingConfig signingCofigs.staging
            productFlavors.blue.signingConfig signingCofigs.release
        }
    }

    productFlavors {
        red {
            applicationId "com.gradleforandroid.red"
            resValue "color", "flavor_color", "#ff0000"
        }

        blue {
            applicationId "com.gradleforandroid.blue"
            resValue "color", "flavor_color", "#0000ff"
        }
    }
}

存储凭证的正确方式

1.在项目根目录创建一个名为private.properties的文件,然后添加一行代码:
//private.properties

release.password = android

2.在build.gradle定义一个名为getReleasePassword的任务:
//build.gradle

android {
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

    signingConfigs { //实际应用中不要在构建配置文件中存储凭证,而是使用Gradle配置文件
        release { //release签名配置
            storeFile file("debug.keystore")
            keyAlias "androiddebugkey"
        }
    }
}

task getReleasePassword << {
    println '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')
        println 'password: ' + password
        android.signingConfigs.release.storePassword = password
        android.signingConfigs.release.keyPassword = password
    }
}

tasks.whenTaskAdded { theTask ->
    if(theTask.name.equals("packageRelease")) {
        theTask.dependsOn "getReleasePassword"
    }
}

自动重命名APK

//build.gradle

android.applicationVariants.all { variant ->
    variant.outputs.all { output ->
        def file = output.outputFile
        outputFileName = file.name.replace(".apk", "-${variant.versionName}-${getTime()}.apk")
    }
}

//获取当前时间
def getTime() {
    String today = new Date().format('YYYY年MM月dd日HH时mm分')
    return today
}

//输出
app-debug-1.0-2019年12月12日18时36分.apk

十、统一管理依赖版本

1.在项目根目录下,新建config.gradle文件:
//config.gradle

ext {
    android = [
            compileSdkVersion: 28,
            buildToolsVersion: "28.0.3",
            applicationId: "com.tomorrow.architetest",
            minSdkVersion: 15,
            targetSdkVersion: 28,
            versionCode: 1,
            versionName: "1.0",
            testInstrumentationRunner: "android.support.test.runner.AndroidJUnitRunner"
    ]

    dependencies = [
            "support-v7"  : 'com.android.support:appcompat-v7:28.0.0'
    ]
}

2.在项目根目录下的build.gradle文件,新增:
//build.gradle

apply from: "config.gradle"

3.在各Module或者Library中的build.gradle文件,进行引用:
//build.gradle

android {
    ...
    compileSdkVersion rootProject.ext.android.compileSdkVersion
    ...
}

相关链接

Gradle

Gradle基本使用
Android Gradle插件主要流程
Gradle源码分析
《Gradle for Android 中文版》

Groovy

Groovy官方文档
Groovy API文档

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容