Gradle构建项目详解

        我们知道Android开发中,Android studio开发平台是通过Gradle构建项目的,那Gradle到底是如何构建生成我们想要的apk文件的呢,而对于一个Android开发人员,关于Gradle相关的知识点,我们应该需要了解那些方面的知识呢,今天我就带大家来了解一下Android开发应该知道的关于Gradle相关知识点吧。


Gradle知识点汇总:

一:Android Studio apk生成的过程简介

二:为什么使用Gradle,Gradle有什么优点

三:Andorid构建项目时,Gradle有哪些相关的文件

四:Grovvy常见基本语法介绍

五:Gradle插件讲解

六:Gradle的生命周期

七:Android中Gradle的常见配置关键字与常见问题汇总(不断更新)

八:如何提高Gradle编译效率

九:扩展阅读


一:Android Studio apk生成的过程简介

Apk生成流程图:

简述:Andorid Studio开发平台通过读取Gradle相关文件,从而开始构建并整合项目中的各种类型的文件,从而一步步的生成Apk文件,而我们可以根据项目的需要,通过配置Gradle文件的内容,从而构建出符合我们自己需求的apk文件。

例如:

1、生成多渠道的apk包(应用包,各大手机应用商店)

2、生成不同开发环境的apk包(dev,sit,uat,prod)


二:为什么使用Gradle,Gradle有什么优点

        Gradle是一个构建工具,那么为什么要用构建工具,这就需要先从项目自动化开始讲起。

        在我们开发软件时,会面临相似的情况就是,我们需要去用IDE来进行编码,当完成一些功能时会进行编译、单元测试、打包等工作,这些工作都需要开发人员手动来实现。而一般的软件都是迭代式开发的,一个版本接着一个版本,每个版本又可能有很多的功能,如果开发每次实现功能时都需要手动的进行编译、单元测试和打包等工作,那显然会非常耗时而且也容易出现问题,因此项目自动化应运而生。


它有以下优点:

一、它可以尽量防止开发手动介入从而节省了开发的时间并减少错误的发生。

二、自动化可以自定义有序的步骤来完成代码的编译、测试和打包等工作,让重复的步骤变得简单。

三、IDE可能受到不同操作系统的限制,而自动化构建是不会依赖于特定的操作系统和IDE的,具有平台无关性。

四:Gradle是一款基于JVM的专注于灵活性和性能的开源构建工具,Gradle的特性与优点如下图:

特性与优点详解:

1、轻松的可拓展性

        Gradle有非常良好的拓展性。如果你想要在多个构建或者项目中分享可重用代码,Gradle的插件会帮助你实现。将Gradle插件应用于你的项目中,它会在你的项目构建过程中提供很多帮助:为你的添加项目的依赖的第三方库、为你的项目添加有用的默认设置和约定(源代码位置、单元测试代码位置)。

2、采用了Groovy

        Ant和Maven的构建脚本是由XML来编写的,如果XML逻辑复杂内容太多就不容易维护。Gradle可以使用Groovy来实现构建脚本,Groovy是基于Jvm一种动态语言,它的语法和Java非常相似并兼容Java,因此你无需担心学习Groovy的成本。Groovy在Java的基础上增加了很多动态类型和灵活的特性,比起XML,Gradle更具有表达性和可读性。

3、强大的依赖管理

       Gradle提供了可配置的可靠的依赖管理方案。一旦依赖的库被下载并存储到本地缓存中,我们的项目就可以使用了。依赖管理很好的实现了在不同的平台和机器上产生相同的构建结果。

4、灵活的约定

        Gradle可以为构建你的项目提供引导和默认值,如果你使用这种约定,你的Gradle构建脚本不会有几行。比起Ant,Gradle不仅仅提供了约定,还可以让你轻松的打破约定。

5、GradleWrapper

       GradleWrapper是对Gradle的包装,它的作用是简化Gradle本身的下载、安装和构建,比如它会在我们没有安装Gradle的情况下,去下载指定版本的Gradle并进行构建。Gradle的版本很多,所以有可能出现版本兼容的问题,这时就需要GradleWrapper去统一Gradle的版本,避免开发团队因为Gradle版本不一致而产生问题。

6、可以和其他构建工具集成

        Gradle可以和Ant、Maven和Ivy进行集成,比如我们可以把Ant的构建脚本导入到Gradle的构建中。

7、底层API

        Gradle显然无法满足所有企业级构建的所有要求,但是可以通过HookGradle的生命周期,来监控和配置构建脚本。

8、社区的支持和推动

        Gradle是一个开源的项目,它遵循了Apache License 2.0协议。Gradle的优良特性吸引了很多开发者并形成了Gradle社区,很多开源软件开发者为Gradle的核心代码做出了共享。


三:Andorid构建项目时,Gradle有哪些相关的文件

1、build.gradle(项目)

2、build.gradle(module)

3、gradle-wrapper.properties

4、config.gradle

5、setting.gradle

6、gradle.properties


GradleWrapper文件:

       GradleWrapper称为Gradle包装器,是对Gradle的一层包装。为什么需要GradleWrapper呢?比如在一个开发团队中,如果每进来一个成员,都需要在计算机中安装Gradle,这个时候运行Gradle的环境和版本就会对构建结果带来不确定性。针对这个问题,Gradle提供了一个解决方案,那就是GradleWrapper,它是一个脚本,可以在计算机没有安装Gradle的情况下运行Gradle构建,并且能够指定Gradle的版本,开发人员可以快速启动并运行Gradle项目,而不必手动安装,这样就标准化了项目,从而提高了开发效率。AS在新建项目时会自带GradleWrapper,这也是我们很少去单独去下载安装Gradle的原因。

   GradleWrapper的工作流程如下图所示:

   gradle-wrapper.properties是GradleWrapper的属性文件,用来配置GradleWrapper,Gradle4.2.1版本对应的gradle-wrapper.properties如下所示。

distributionBase=GRADLE_USER_HOME

distributionPath=wrapper/dists

distributionUrl=https\://services.gradle.org/distributions/gradle-4.2.1-bin.zip

zipStoreBase=GRADLE_USER_HOME

zipStorePath=wrapper/dists

字段的含义解析:

distributionBase:Gradle解包后存储的主目录。

distributionPath:distributionBase指定目录的子目录。distributionBase+distributionPath就是Gradle解包后的存放位置。

distributionUrl:Gradle发行版压缩包的下载地址。

zipStoreBase:Gradle压缩包存储主目录。

zipStorePath:zipStoreBase指定目录的子目录。zipStoreBase+zipStorePath就是Gradle压缩包的存放位置。

        这里我们最需要关注的是distributionUrl这个字段,如果官方的地址下载不了或者缓慢,可以将这个地址换为其他的镜像地址,或者干脆把Gradle发行版压缩包放在服务器上以供下载。


四:grovvy常见基本语法介绍

简述:Groovy是Apache旗下的一种基于JVM的面向对象编程语言,既可以用于面向对象编程,也可以用作纯粹的脚本语言,在语言的设计上它吸纳了Python、Ruby和Smalltalk语言的优秀特性,比如动态类型转换、闭包和元编程支持。

        Groovy与Java可以很好的互相调用并结合编程 ,比如在写Groovy的时候忘记了语法可以直接按Java的语法继续写,也可以在Java中调用Groovy脚本。比起Java,Groovy语法更加的灵活和简洁,可以用更少的代码来实现Java实现的同样功能。

       项目构建比较复杂,为了使用各种开发语言的开发者都能够快速的构建项目,专家们开发出了Gradle这个基于Groovy的DSL,DSL(DomainSpecifcLanguage)意为领域特定语言,只用于某个特定的领域。

        我们只要按照Groovy的DSL语法来写,就可以轻松构建项目。

语法一:变量

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

   def a = 1;

   def int b = 1;

   def c = "hello world";


语法二:方法

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

   用def关键字定义方法。

   task method <<{

       add (1,2)

       minus 1,2 //1

   }

   def add(int a,int b) {

       println a+b  //3

   }

   def minus(a,b) {   //2

       println  a-b

   }

如果指定了方法返回类型,可以不需要def关键字来定义方法。

   task method <<{

       def number=minus 1,2

       println number

   }

   int  minus(a,b) {

       return a-b

   }

   如果不使用return,方法的返回值为最后一行代码的执行结果。

   int minus(a,b) {

       a-b  //4

   }

从上面两段代码中可以发现Groovy中有很多省略的地方:

1、语句后面的分号可以省略。

2、方法的括号可以省略,比如注释1和注释3处。

3、参数类型可以省略,比如注释2处。

4、return可以省略掉,比如注释4处。


语法三:类

   Groovy类非常类似于Java类。

   task method <<{

       def p = new Person()

       p.increaseAge 5

       printlnp.age

   }

   class Person {

       String name

       Integer age =10

       def increaseAge(Integer years) {

           this.age+= years

       }

   }

   运行gradlemethod打印结果为:15

Groovy类与Java类有以下的区别:

1、默认类的修饰符为public。

2、没有可见性修饰符的字段会自动生成对应的setter和getter方法。

3、类不需要与它的源文件有相同的名称,但还是建议采用相同的名称。


语法四:语句

4.1、断言

     Groovy断言和Java断言不同,它一直处于开启状态,是进行单元测试的首选方式。

   task method <<{

       assert 1+2 == 6

   }

   输出结果为:

   Execution failed for task ':method'.

           > assert 1+2 == 6

           | |

           3 false

解析:当断言的条件为false时,程序会抛出异常,不再执行下面的代码,从输出可以很清晰的看到发生错误的地方。


4.2、for循环

      Groovy支持Java的for(int i=0;i

//遍历范围

   def x = 0

for (iin 0..3 ) {

       x += I }

assert x == 6

//遍历列表

   def x = 0

for (iin [0, 1, 2, 3] ) {

       x += I }

assert x == 6

//遍历Map中的值

   def map = ['a':1, 'b':2, 'c':3]

   x = 0

   for ( v inmap.values() ) {

       x += v }

assert x == 6


4.3、switch语句

   Groovy中的Switch语句不仅兼容Java代码,还可以处理更多类型的case表达式。

   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"

       }

       assert result == "range"

   }

解析:case表达式可以是字符串、列表、范围、Integer等等,因为篇幅原因,这里只列出了一小部分。


语法五:数据类型

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

1、Java中的基本数据类型

2、Groovy中的容器类

3、闭包


5.1、字符串

      Groovy中的基本数据类型和Java大同小异,这里主要介绍下字符串类型。

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

5.1.1:单引号字符串

       在Groovy中单引号字符串和双引号字符串都可以定义一个字符串常量,只不过单引号字符串不支持插值。( 'Android进阶解密‘)

5.1.2:双引号字符串

     要想插值可以使用双引号字符串,插值指的是替换字符串中的占位符,占位符表达式为${}或者以$为前缀。

   def name = 'Android进阶之光'

   println "hello ${name}"

   println "hello $name"

5.1.3:三引号字符串

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

   task method <<{

       def name = '''Android进阶之光

       Android进阶解密

       Android进阶?'''

       println name

   }

   打印结果为:

   Android进阶之光

           Android进阶解密

   Android进阶?


5.1. 4:GString

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

   assert "one: ${1}".hashCode() != "one: 1".hashCode()

       当双引号字符串中包含插值表达式时,字符串类型为GString,因此上面的断言为true。


5.2、List

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

   def number = [1, 2, 3]

   assert number instance of List

   def linkedList= [1, 2, 3]  as LinkedList   

   assert linkedList instanceof java.util.LinkedList

   可以使用as操作符来显式指定List的实现类为java.util.LinkedList。

   获取元素同样要比Java要简洁些,使用[]来获取List中具有正索引或负索引的元素。

   task method <<{

       def number = [1, 2, 3, 4]

       assert number [1] == 2

       assert number [-1] == 4  //1 

       number << 5    //2            

       assert number [4] == 5

       assert number [-1] == 5

   }

   注释1处的索引-1是列表末尾的第一个元素。注释2处使用<<运算符在列表末尾追加一个元素。


5.3、Map

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

   def name = [one: '魏无羡', two: '杨影枫', three: '张无忌']

           assert name['one'] == '魏无羡'

           assertname.two == '杨影枫‘

   Map还有一个键关联的问题:

   def key = 'name'

   def person = [key: '魏无羡']   //1

           assert person.containsKey('key')

   person = [(key): '魏无羡']     //2      

           assertperson.containsKey('name')

   注释1处魏无羡的键值是key这个字符串,而不是key变量的值name。如果想要以key变量的值为键值,需要像注释2处一样使用(key),用来告诉解析器我们传递的是一个变量,而不是定义一个字符串键值。


5.4 闭包(Closure)

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


   闭包的定义遵循以下语法:

{ [closureParameters-> ] statements }

   闭包分为两个部分,分别是参数列表部分[closureParameters-> ]和语句部分statements。

   参数列表部分是可选的,如果闭包只有一个参数,参数名是可选的,Groovy会隐式指定it作为参数名,如下所示。

{println it }    //使用隐式参数it的闭包

当需要指定参数列表时,需要->将参数列表和闭包体相分离。

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

{ String a, String b ->                               

   println "${a} is a ${b}"

}

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

//将闭包赋值给一个变量

def println={ it ->println it }    

assert printlnin stanceofClosure

//将闭包赋值给Closure类型变量

Closure do= {println 'do!' }

调用闭包

闭包既可以当做方法来调用,也可以显示调用call方法。

def code = { 123 }

assert code() == 123 //闭包当做方法调用

assert code.call() == 123 //显示调用call方法

def isOddNumber= {inti-> i%2 != 0 }                          

assert isOddNumber(3) == true //调用带参数的闭包


6、I/O操作

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

6.1 文件读取

   我们可以在PC上新建一个name.txt,在里面输入一些内容,然后用Groovy来读取该文件的内容:

def filePath= "D:/Android/name.txt"

def file = new File(filePath) ;

file.eachLine{

   printlnit

}

可以看出Groovy的文件读取是很简洁的,还可以更简洁些:

def filePath= "D:/Android/name.txt"

def file = new File(filePath) ;

println file.text


6.2 文件写入

文件写入同样十分简洁:

def filePath= "D:/Android/name.txt"

def file = new File(filePath);

file.withPrintWriter{

   it.println("三井寿")

   it.println("仙道彰")

}


7. 其他

7.1 asType

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

String a = '23'

int b = a as int

def c =a.asType(Integer)

assert c instanceof java.lang.Integer


7.2 判断是否为真

if (name != null &&name.length> 0) {}

可以替换为

if (name) {}


7.3 安全取值

     在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


7.4  with操作符

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

task method <<{

Person p = new Person()

p.name = "杨影枫"

p.age= 19

p.sex= "男"

println p.name

}

class Person {                      

   String name                     

   Integer age

   String sex

}

使用with来进行简化:

Person p = new Person()

p.with{

  name = "杨影枫"

  age= 19

  sex= "男"

 }  

println p.name

8. 总结

      大概的介绍了Groovy的一些语法,包括:变量、方法、数据类型等等,比起Groovy官方文档来说,介绍的并不多,但不要忘了本系列的目标是学习与Android相关的Gradle,Groovy并不是重点,我们只需要了解本文所介绍的内容就够了,如果碰到哪里不会再去查找Groovy官方文档和Groovy API文档。

例子:

 task hello {

       doLast{

           println 'Hello world!'

       }

   }

简洁版:

 task hello << {

       println 'Hello world!'

   }

解析:

     task(任务)和action(动作)是Gradle的重要元素。上面的代码中,task代表一个独立的原子性操作,比如复制一个文件,编译一次Java代码,这里我们简单的定义一个名为hello的任务。doLast 代表task执行的最后一个action,通俗来讲就是task执行完毕后会回调doLast中的代码,在上面这个Gradle的任务,Gradle的任务包括创建任务、任务依赖、动态定义任务和任务的分组和描述。


创建任务

方法一:直接用任务名称创建

   def Task hello=task(hello)

   hello.doLast{

       println"hello world"

   }

方法二:任务名称+任务配置创建

   def Task hello=task(hello,group:BasePlugin.BUILD_GROUP)

   hello.doLast{

       println"hello world"

   }

    其中group为任务配置项,它代表了分组,关于分组具体见3.4小节。

方法三:TaskContainer的create方法创建

   tasks.create(name: 'hello') << {

       println"hello world"

   }

     此前创建任务的方式最终都会调用tasks的create方法,其中tasks类型为TaskContainer。

任务依赖:任务依赖会决定任务运行的先后顺序,被依赖的任务会在定义依赖的任务之前执行。创建任务间的依赖关系如下所示。

   task hello << {

       println'Hello world!'

   }

   task go(dependsOn: hello) << {

       println"go for it"

   }

      在hello任务的基础上增加了一个名为go的任务,通过dependsOn来指定依赖的任务为hello,因此go任务运行在hello之后。

   运行gradle -q go构建脚本,打印结果如下:

   Hello world!

   go for it


动态定义任务:动态定义任务指的是在运行时来定义任务的名称,如下所示。

           3.times {number ->

           task "task$number" << {

           println "task $number"

   }

   }

解析:这里用到了Groovy语法,关于Groovy语法会在本系列后续的文章进行介绍。times是Groovy在java.lang.Number中拓展的方法,是一个定时器。3.times中循环创建了三个新任务,隐式变量number的值为0,1,2,任务的名称由task加上number的值组成,达到了动态定义任务的目的。

   运行gradle -q task0构建脚本,打印结果如下:

   task 0


任务的分组和描述

      Gradle有任务组的概念,可以为任务配置分组和描述,以便于更好的管理任务,拥有良好的可读性。改造上面的例子,为hello任务添加分组和描述。

   task hello {

       group = 'build'

       description = 'hello world'

       doLast{

           println"任务分组: ${group}"

           println"任务描述: ${description}"

       }

   }

   task go(dependsOn: hello) << {

       println"go for it"

   }

也可以采用3.1小节中其他的创建任务方式来为任务添加分组和描述,如下所示。

   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"

   }


五:Gradle插件讲解

5.1、应用Gradle插件

        要想应用插件,主要有两个步骤,一是解析插件,二是把插件应用到项目中,应用插件通过 Project.apply()方法来完成。

        在Gradle中一般有两种类型的插件,分别叫做脚本插件和对象插件。脚本插件是额外的构建脚本,它会进一步配置构建,可以把它理解为一个普通的build.gradle。对象插件又叫做二进制插件,是实现了Plugin接口的类。


2.1脚本插件

文件other.gradle:

ext{

verson='1.0'

url='http://liuwangshu.cn'

}

这实际上不算是一个真正的脚本插件,就是一个简单的脚本,主要是用于演示脚本插件是如何被应用的。

    我们在build.gradle中来应用这个插件:

build.gradle

apply from: 'other.gradle'

task test {

   doLast{

       println"版本为:${verson},地址为:${url}"

   }

}

      apply是Gradle project中提供的方法,用于配置项目中的插件。执行gradlew.bat test,会打印出想要的结果。

对象插件:我们知道对象插件就是实现了org.gradle.api.plugins接口的插件,对象插件可以分为内部插件和第三方插件。


2.2.1 内部插件

      如果我们想要应用Java插件可以这么写:

build.gradle文件:

apply plugin:org.gradle.api.plugins.JavaPlugin

Gradle默认就导入了org.gradle.api.plugins包,因此我们也可以去掉包名:

apply plugin:JavaPlugin

       实现了org.gradle.api.plugins接口的插件会有pulginid,使用pulginid是最简洁、最常用的方式:apply plugin: 'java‘

       Gradle的发行包中有大量的插件,这些插件有很多类型,比如语言插件、集成插件、软件开发插件等等,如果我们想向项目添加c++源代码编译功能,可以这么写:

        apply plugin: 'cpp'


第三方插件:

   第三方的对象插件通常是jar文件,要想让构建脚本知道第三方插件的存在,需要使用buildscript来设置。

buildscript{

 repositories {

   maven {

     url"https://plugins.gradle.org/m2/"

   }

 }

 dependencies {

   classpath"com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4"

 }

}

apply plugin: "com.jfrog.bintray"

      在buildscript中来定义插件所在的原始仓库和插件的依赖 ,再通过apply方法配置就可以了。Android Gradle插件也属于第三方插件,如果我们想引入Android Gradle插件,可以这么写:

buildscript{

   repositories {

       jcenter()

   }

   dependencies {

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

   }

}

apply plugin: 'com.android.application'

      这样我们就可以使用Android Gradle插件,通过apply方法来使用App工程插件,这样项目会编译成为一个apk。

自定义对象插件:

        对象插件是实现了org.gradle.api.plugins接口的插件,这个接口中只定义个一个简单的apply方法,想要自定义插件就需要去实现org.gradle.api.plugins接口。

        来实现一个简单的自定义插件,为了方便测试,不再采用文本编辑,而是使用IntelliJ来编辑(AS也可以),用IntelliJ来打开2.1小节的例子,改写build.gradle文件:

build.gradle

applyplugin:CustomPlugin

classCustomPluginimplements Plugin {

   @Override

   void apply(Project project) {

       project.task('CustomPluginTask') {

           doLast{

               println"自定义插件"

           }

       }

   }

}

       在build.gradle中自定义了一个插件CustomPlugin,在apply方法中创建一个名称为CustomPluginTask的任务。在IntelliJ的Terminal中输入gradlew.bat CustomPluginTask来执行CustomPluginTask任务。


Gradle插件的作用和好处:

Gradle插件可以做什么呢?主要有以下的几点

1、为项目配置依赖。

2、为项目配置约定,比如约定源代码的存放位置。

3、为项目添加任务,完成测试、编译、打包等任务。

4、为项目中的核心对象和其他插件的对象添加拓展类型。


使用Gradle插件主要有以下的好处:

1、重用和减少维护在多个项目类似的逻辑的开销。

2、更高程度的模块化。

3、封装必要的逻辑,并允许构建脚本尽可能是声明性。


六:Gradle的生命周期

Gradle构建生命周期分三个阶段:

   初始化阶段:负责判断有多少个Projects参与构建。

   配置阶段:负责对初始化阶段创建的Projects完成配置。

   执行阶段:根据配置阶段的配置执行任务。


初始化阶段:

       构建初始化阶段首先寻找一个叫settings.gradle的文件,检查是否当前构建是否是多项目构建,并负责创建项目树。在多项目构建中,settings.gradle是必需的,因为这个文件定义了参与构建的项目,通过settings.gradle判断有哪些项目需要初始化,加载所有需要初始化的项目的build.gradle文件并为每个项目创建project对象。

        可以看出,settings.gradle中的代码最先执行,所以理论上还可以做其他事情。

  而在没有settings.gradle文件的项目中,如果执行构建,则Gradle按这个顺序查找settings.gradle:

1、从当前目录的master文件夹内寻找。

2、如果master目录中也没有,则搜索父目录。

3、如果父目录也没找到,则把构建当成单个项目构建。

4、如果找到了,并且发现当前项目是多项目构建的一部分,则执行多项目构建。没找到,则执行单项目构建。

        从第二步中可以看出,Gradle支持从子项目中触发构建父项目。如果不想从子项目触发父项目,而只是做单项目构建,则应该在gradle命令后加上-u命令行选项。


配置阶段:

        配置阶段负责对初始化阶段创建的projects做一些配置,比如添加Task,修改Task的行为等,执行各项目下的build.gradle脚本,完成project的配置,并且构造Task任务依赖关系图以便在执行阶段按照依赖关系执行Task.执行task中的配置代码。


执行阶段:

       通过配置阶段的Task图,按顺序执行需要执行的任务中的动作代码,就是执行任务中写在doFirst或doLast中的代码。

       以上就是Gradle构建项目时的生命周期,由于Gradle的强大和易配性,如果想在构建的过程中去做一些额外的操作的话,可以使用Gradle自带的钩子方法,Gradle提供了很多生命周期监听方法,可以在各个阶段Hook指定的任务。

执行流程图:

备注:这里有几个概念很关键,gradle, project, task。

1、gradle全局可见,相当于这个gradle工具本身。

2、project是一个模块,与android studio中的module是对应的关系,也对应了一个build.gradle的文件。

3、task是最小的执行块。为了方便,我们来有两个project的工程来模拟,他们的具体执行顺序进一步细分为下面的这个样子。

图解:

Project

   Project提供的生命周期回调方法有:

   //在Project进行配置前调用

   void beforeEvaluate(Closure closure)

   //在Project配置结束后调用

   void afterEvaluate(Closure closure)

      beforeEvaluate必须在父模块的build.gradle对子模块进行配置才能生效,因为在当前模块的build.gradle中配置,它自己本身都没配置好,所以不会监听到。


Gradle

   Gradle提供的生命周期回调方法很多,部分与Project里的功能雷同:

//在project进行配置前调用,child project必须在root project中设置才会生效,root project必须在settings.gradle中设置才会生效。

   void beforeProject(Closure closure)

//在project配置后调用

   afterProject(Closure closure)

//构建开始前调用

   void buildStarted(Closure closure)

//构建结束后调用

   void buildFinished(Closure closure)

//所有project配置完成后调用

   void projectsEvaluated(Closure closure)

//当settings.gradle中引入的所有project都被创建好后调用,只在该文件设置才会生效

   void projectsLoaded(Closure closure)

//settings.gradle配置完后调用,只对settings.gradle设置生效

   void settingsEvaluated(Closure closure)

我们修改setting.gradle的代码如下:

   gradle.settingsEvaluated{

       println "settings:执行settingsEvaluated..."}

   gradle.projectsLoaded{

       println"settings:执行projectsLoaded..."}

   gradle.projectsEvaluated{

       println"settings:执行projectsEvaluated..."}

   gradle.beforeProject{proj->

           println"settings:执行${proj.name}beforeProject"}

   gradle.afterProject{proj->

           println"settings:执行${proj.name}afterProject"}

   gradle.buildStarted{

       println"构建开始..."}

   gradle.buildFinished{

       println"构建结束..."}

   include  ":app"

这个时候的执行结果如下:

   settings:执行settingsEvaluated...

   settings:执行projectsLoaded...

   settings:执行testbeforeProject

   根项目配置开始---

   根项目里任务配置---

   根项目配置结束---

   settings:执行test afterProject

   settings:执行app beforeProject

   子项目beforeEvaluate回调...

   APP子项目配置开始---

   APP子项目里任务配置---

   APP子项目配置结束---

   settings:执行app afterProject

   APP子项目after Evaluate回调...

   settings:执行projects Evaluated...

   构建结束...

Gradle构建周期中的Hook点:

七:Android中Gradle的常见配置关键字与常见问题汇总(不断更新)

一、Gradle是什么

1、Gradle是一个自动化构建工具

2、兼容Maven等仓库

3、基于Groovy的特定领域语言来声明设置


二、GradleWraper的作用是什么

1、GradleWrapper是一个脚本文件

2、它会在没有安装Gradle的情况下为我们下载Gradle,之后我们就可以使用gradlew命令,像使用gradle一样来使用Gradle了

3、GradleWraper简化了gradle的安装部署


三:Gradle命令

1、gradlew clean:清除app目录下的build文件夹

2、gradlew check:执行lint检查

3、gradlew assemble:打release和debug包

4、gradlew build: 执行check和assemble

5、gradlew assembleRelease/gradlewassembleDebug:打全部渠道的Release或者debug包


四:设置编译android项目的参数

       android {

       //编译SDK的版本

       compileSdkVersion22

       // build tools的版本

       buildToolsVersion"23.0.1"

       //aapt配置

       aaptOptions{

       //不用压缩的文件

       noCompress'pak', 'dat', 'bin', 'notice'

       //打包时候要忽略的文件

       ignoreAssetsPattern"!.svn:!.git"

       //分包

       multiDexEnabled true

       //--extra-packages是为资源文件设置别名:意思是通过该应用包名+R,com.android.test1.R和com.android.test2.R都可以访问到资源

       additionalParameters'--extra-packages', 'com.android.test1','--extra-packages','com.android.test2'

       }

       //默认配置

       defaultConfig{

       //应用的包名

       applicationId "com.example.heqiang.androiddemo"

       minSdkVersion 21

       targetSdkVersion 22

       versionCode 1

       versionName "1.0"

       }

       //编译配置

       compileOptions{

       // java版本

       sourceCompatibilityJavaVersion.VERSION_1_7

       targetCompatibilityJavaVersion.VERSION_1_7

       }

       //源文件目录设置

       sourceSets{

       main {

       //jnilib的位置

       jniLibs.srcDirs=jniLibs.srcDirs<< 'src/jniLibs'

       //定义多个资源文件夹,这种情况下,两个资源文件夹具有相同优先级,即如果一个资源在两个文件夹都声明了,合并会报错。

       res.srcDirs= ['src/main/res', 'src/main/res2']

       //指定多个源文件目录

       java.srcDirs= ['src/main/java', 'src/main/aidl']

       }

       }

       //签名配置

       signingConfigs{

       debug {

       keyAlias 'androiddebugkey'

       keyPassword 'android'

       storeFilefile('keystore/debug.keystore')

       storePassword 'android'

       }

       }

buildTypes{

       //release版本配置

       release {

       debuggable false

       //是否进行混淆

       minifyEnabled true

       //去除没有用到的资源文件,要求minifyEnabled为true才生效

       shrinkResources true

       //混淆文件的位置

       proguardFilesgetDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'

       signingConfigsigningConfigs.debug

       //ndk的一些相关配置,也可以放到defaultConfig里面。

       //指定要ndk需要兼容的架构(这样其他依赖包里mips,x86,arm-v8之类的so会被过滤掉)

       ndk{

       abiFilter "armeabi"

       }

       }

//debug版本配置

       debug {

       debuggable true

       //是否进行混淆

       minifyEnabled false

       //去除没有用到的资源文件,要求minifyEnabled为true才生效

       shrinkResources true

       //混淆文件的位置

       proguardFilesgetDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'

       signingConfig signingConfigs.debug

       //ndk的一些相关配置,也可以放到defaultConfig里面。

       //指定要ndk需要兼容的架构(这样其他依赖包里mips,x86,arm-v8之类的so会被过滤掉)

       ndk{

       abiFilter"armeabi"

       }

       }

       }

       // lint配置

       lintOptions{

       //移除lint检查的error

       abortOnErrorfalse

       //禁止掉某些lint检查

       disable 'NewApi'

       }

       }

备注:android中还可以有以下配置:productFlavors{ }产品风格配置,ProductFlavor类型;testOptions{ }测试配置,TestOptions类型;dexOptions{ }dex配置,DexOptions类型;packagingOptions{ }PackagingOptions类型;jacoco{ }JacocoExtension类型。 用于设定jacoco版本;splits{ } Splits类型。


五:几种依赖的区别


六:排除依赖传递,解决依赖冲突

1、exclude:设置不编译指定的模块,排除指定模块的依赖

2、transitive:用于自动处理子依赖项,默认为true,gradle自动添加子依赖项。设置为false排除所有的传递依赖

3、force:强制设置某个模块的版本。

常见代码实现:

implementation ('io.reactivex.rxjava2:rxandroid:2.1.0') {

       exclude group: 'io.reactivex.rxjava2', module: 'rxjava'

   }


依赖替换规则

  依赖替换规则的适用场景分为以下几种:

1.根据某些条件对依赖进行替换;

2.将本地依赖替换为外部依赖;

3.将外部依赖替换为本地依赖;


外部依赖和本地依赖是什么。

外部依赖:外部依赖,顾名思义,就是从远程仓库拉取的依赖,也被称为常用的三方库:

//从远程仓库拉取的开源代码库

implementation 'com.facebook.stetho:stetho:1.5.1'

implementation 'io.reactivex.rxjava3:rxjava:3.0.0-RC0‘


本地依赖:也就是我们项目中常见的module,按照**《阿里Java开发手册》**中来描述,也叫做一方库:

implementation project(':library')

       根据某些条件对依赖进行替换举个例子,很多UI三方库都会依赖RecyclerView,但这么多的依赖库,我们不可避免遇到版本不同导致依赖冲突的情况,一般情况下,我们是这么解决的:

           //将RecyclerView的依赖从这个三方库中排除掉

           implementation "xxx.xxx:xxx:1.0.0",{

           exclude group: 'com.android.support', module: 'recyclerview-v7'

       }

         将RecyclerView的依赖从这个三方库中排除掉,令其使用项目本身的RecyclerView版本,这样项目就可以正常运行了,看起来并没有什么问题。

试想,如果项目的依赖比较复杂,也许我们要面对的将是这样的依赖配置:

       implementation "libraryA:xxx:1.0.0",{

       exclude group: 'com.android.support', module: 'recyclerview-v7'

       }

       implementation "libraryB:xxx:2.2.0",{

       exclude group: 'com.android.support', module: 'recyclerview-v7'

       }

       implementation "libraryC:xxx:0.0.8",{

       exclude group: 'com.android.support', module: 'recyclerview-v7'

       }

将外部依赖替换为本地依赖

       该规则和2非常相似,只不过将依赖替换的双方调换了而已,下面是官方的示例代码:

       configurations.all{

       resolutionStrategy.dependencySubstitution{

       substitute module("org.utils:api") because "we work with the unreleased development version" with project(":api")

       substitute module("org.utils:util:2.5") with project(":util")

       }

       }

       1.因为group不同,所以需要先将2.x的rxjava全局exclude掉;

       2.将所有3.x的rxjava的依赖版本都统一(文中是3.0.0-RC0)


七:Gradle打包时的Proguard

1、通过在buildTypes中配置minifyEnable来开启和关闭proguard

2、通过proguardFiles来配置混淆参数与keep的内容


混淆的作用:

1、压缩(Shrink):检测并移除代码中无用的类、字段、方法和特性(Attribute)。

2、优化(Optimize):对字节码进行优化,移除无用的指令。

3、混淆(Obfuscate):使用a,b,c,d这样简短而无意义的名称,对类、字段和方法进行重命名。

4、预检(Preveirfy):在Java平台上对处理后的代码进行预检,确保加载的class文件是可执行的。

哪些不做混淆:

1、Android系统组件

2、JNI

3、反射

4、WebView的JS调用

5、内部类

6、Annottation

7、enum

8、范型

9、序列化

10、第三方


八:多渠道打包

原理:在AndroidManifest.xml配置mete-data

       android:name="UMENG_CHANNEL"

       android:value="Channel_ID" />

配置Flavors:

android {

productFlavors{

       xiaomi{

       manifestPlaceholders= [UMENG_CHANNEL_VALUE: "xiaomi"]}

       _360 {

       manifestPlaceholders= [UMENG_CHANNEL_VALUE: "_360"]}

       baidu{

       manifestPlaceholders= [UMENG_CHANNEL_VALUE: "baidu"]}

       wandoujia{

       manifestPlaceholders= [UMENG_CHANNEL_VALUE: "wandoujia"]}

       }

}

批量修改:

android {

  productFlavors{

       xiaomi{}

       _360 {}

       baidu{}

       wandoujia{}

}

productFlavors.all{

       flavor ->flavor.manifestPlaceholders= [UMENG_CHANNEL_VALUE: name]

       }}

     在APP内读取mete-data配置确定渠道

      然后用./gradlewassembleRelease这条命令会把Product Flavor下的所有渠道的Release版本都打出来。

       因为以上方法需要多次编译,速度较慢,当渠道变多之后不适合多渠道打包

改进的方法1:apk反编译后重写AndroidManifest文件,再重新编译签名

改进的方法2:如果在META-INF目录内添加空文件,可以不用重新签名应用。因此,通过为不同渠道的应用添加不同的空文件,可以唯一标识一个渠道,因为v1签名可以在不改变签名情况下二次打包,我们可以在gradle中对dex文件进行自己的签名。

   在采用V2签名后,以上方法不再适用,考虑到V2签名的特点(对APK Signing Block是不进行验证的),我们向V2签名后的APK签名区块写入渠道号,实现多渠道打包。


备注:在多渠道批量打包方面,可以参考使用:腾讯VasDolly,美团Walle。

九:如何提高Gradle编译效率

备注:上面是谷歌官方推荐的提升Gradle编译速度的建议,具体详情可以去扩展阅读中查看。


十:扩展阅读

1、https://blog.csdn.net/itachi85/article/details/81906139(刘望舒系列)

2、https://www.jianshu.com/p/2e19268bf387(AndroidGradle学习(七):Gradle构建生命周期)

3、https://blog.csdn.net/woxueliuyun/article/details/54602701(Gradle生命周期)

4、https://blog.csdn.net/u011486491/article/details/79001186(AndroidGradle学习2——gradle生命周期和重要的gradle)

5、https://blog.csdn.net/coloriy/article/details/52807393(AndroidGradle看这一篇就够了)

6、https://www.jianshu.com/p/c11862136abf(史上最全Androidbuild.gradle配置详解)

7、https://www.jianshu.com/p/1274c1f1b6a4(要点提炼|Gradle指南)

8、https://www.jianshu.com/p/2bd920314012(AndroidGradle干货)

9、https://www.jianshu.com/p/dcf14b220a19(不一样的Gradle多渠道配置总结)

10、https://www.jianshu.com/p/d84032b46b56(如何开发一款高性能的gradletransform)

11、https://juejin.im/post/5d2dee0851882569755f5494(JakeWharton评价我的代码像是在打地鼠)

12、https://www.jianshu.com/p/b7a6ee994a52(Android应用构建速度提升的十个小技巧)

13、https://blog.csdn.net/weixin_34290352/article/details/88009764(Android面试题之Gradle配置篇)

你可能感兴趣的:(Gradle构建项目详解)