美文网首页Android进阶之路
【Gradle深入浅出】——Gradle基础概念

【Gradle深入浅出】——Gradle基础概念

作者: 被代码淹没的小伙子 | 来源:发表于2020-11-29 23:34 被阅读0次

    系列目录

    1.【Gradle深入浅出】——初识Gradle
    2.【Gradle深入浅出】——Gradle基础概念

    前言

    前一篇博客从基础层面对Gradle做了一个讲解,让我们对于Gradle有了一个大体上对认知。本篇博客开始就开始进入Gradle的学习了,本篇博客将从Gradle的基础概念进行讲解,Gradle的基础概念又分为Gradle和AGP(Android Gradle Plugin后续简称AGP)的基础概念,所以这里会做一定的划分,可能有的概念在两边都有涉及,所以区分不会特别明显,但是对于我们学习来说,并不会造成什么困扰。

    Gradle基础概念

    首先还是回到上篇博客一直围绕的一个话题,Gradle是什么?
    Gradle中的所有内容都基于两个基本概念:project和task
    Gradle 是通过组织一系列 task 来最终完成自动化构建的,所以 task 是 Gradle 里最重要的概念
    我们以生成一个可用的 apk 为例,整个过程要经过 资源的处理,javac 编译,dex 打包,apk 打包,签名等等步骤,每个步骤就对应到 gradle 里的一个 task
    gradle 可以类比做一条流水线,task 可以比作流水线上的机器人,每个机器人负责不同的事情,最终生成完整的构建产物。


    流水线

    而Gradle的代码实质是配置脚本,执行一种类型的配置脚本时就会创建一个关联的对象。
    Gradle的三种主要对象解释如下:

    • Project对象:每个build.gradle会转换成一个Project对象。
    • Gradle对象:构建初始化时创建,整个构建执行过程中只有这么一个对象,一般很少去修改这个默认配置脚本。
    • Settings对象:每个settings.gradle会转换成一个Settings对象。

    Build的生命周期

    Gradle构建的生命周期其实相对来说还比较复杂,这里先仅从大方面来讲一下Gradle的生命周期,对于后续我们的理解有帮助,而对于具体Gradle提供的生命周期hook在后面再专门讲解。
    Gradle的构建大体分为三个阶段:

    • 初始化阶段
      初始化阶段主要做的事情是有哪些项目需要被构建,也就是执行我们的setting.gradle,构建出Setting对象,并且创建对应的Product对象。
    • 配置阶段
      配置阶段主要是根据上一步setting.gradle配置的项目,根据项目的build.grale进行构建,这时候就会根据build.gradle,并且生成相应要执行的Task
    • 执行阶段
      执行阶段就是根据上面的task,按照顺序进行执行构建。

    Setting对象

    上篇博客其实有提到,我们在多项目构建多时候,Setting.gradle就发挥了很大多作用,这个文件一般放在工程多根目录,该文件在初始化阶段被执行,通过读取我们在setting.gradle中配置的多项目,引入并且进行构建,所以我们在做组件化和模块化的时候,经常就是在setting.gradle里面做文章。

    Project/RootProject/SubProject

    每次构建(build)至少由一个project构成,我们每个build.gradle脚本在被Gradle解析后,都会生成一个Project对象。
    而这里要讲解下RootProject/SubProject的区别,其实也很好理解,上一篇博客其实就有提到,我们一个项目里有多个build.gradle,所以对应的肯定有多个project对象,而根目录下的build.gradle对应的就是RootProject,每个子module下的build.gradle对应的就是SubProject.我们可以输入命令:./gradlew projects来查看当前有的Project对象。

    Root project 'StudyDemo'
    \--- Project ':app'
    

    关于Project相关的配置,后面会专门开一篇博客进行讲解,本篇的博客的重点还是从代码的角度,来了解下Gradle的实质。

    Task

    每个task的实质其实是一些更加细化的构建(譬如编译class、创建jar文件等)。所以Task正如其名,表示一个任务,那我们用具体的代码来看下Task具体是怎么样的。

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

    当我们在build.gradle中定义了上述Task后,我们通过./gradlew hello执行task,就会发现输出结果。

    Task :hello
    Hello world

    Task的创建方式

    而创建Task有很多种方式,这里列举一下

    //创建一个名为build.gradle的文件
    task hello {
        doLast {
            println 'Hello world!'
        }
    }
    
    //这是快捷写法,用<<替换doLast
    task hello2 << {
        println 'Hello world!'
    }
    
    task (hello3){
      println 'Hello3 world!'
    }
    
    task ('hello4'){
      println 'Hello4 world!'
    }
    
    tasks.create('hello5'){
      doLast{
        println 'Hello5 world!'
      }
    }
    

    Task的执行阶段

    而这里我们继续展开下,从上面的几种创建方式,我们会发现其实是有些区别的,为什么有些有doLast是什么,而有些没有,有些有<<这样的符号,这种会有区别吗?

    task hello {
      println 'init here'
      doLast {
        println 'Hello world'
        }
    }
    

    通过上面这个例子我们来理解下,这里我们分别执行两个命令,第一个gradle -q会发现结果是这样的。

    init here
    Welcome to Gradle 6.1.1.
    To run a build, run gradle <task> ...
    To see a list of available tasks, run gradle tasks
    To see a list of command-line options, run gradle --help
    To see more detail about a task, run gradle help --task <task>
    For troubleshooting, visit https://help.gradle.org

    通过输出的日志对比我们应该会发现区别,我们在没有执行task的时候,我们写入的init here同样打印来出来,而我们doLast中的逻辑并没有执行。
    所以这里就可以看出区别,如果我们定义了一个Task没有加doLast或者没有使用<<的话,task内部的内容无论执行什么task都会在inittialization阶段被执行,而<<其实是doLast的简化,所以当我们用doLast定义的内容,则会在该Task被执行的时候执行。

    自定义Task

    Task其实就是一个对象,而且前面说到了Groovy和Java是互通的,我们是可以自定义Task的,做一定程度的组合和封装。

    class MyTask extends DefaultTask {
    
        @TaskAction
        void action1(){
            println 'do action1'
        }
        
        @TaskAction
        void action2(){
            println 'do action2'
        }
    
        void action3(){
            println 'do action3'
        }
    }
    
    task hello3 (type:MyTask){
        doLast{
            println 'do Last'
        }
    }
    
    

    同样我们来执行下hello3 Tast,会得到下面的结果。

    Task :hello3
    do action1
    do action2
    do Last

    所以这里就会明白,自定义Task我们首先需要集成DefaultTask对象,并且实现方法,方法使用@TaskAction进行注解,被注解的方法会按照定义方法的顺序在Task被执行的时候执行,而如果没有使用注解,则就是一个常规的对象方法。我们可以看下@TaskAction的源码。

    /**
     * Marks a method as the action to run when the task is executed.
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    @Inherited
    public @interface TaskAction {
    }
    

    Task的顺序

    前面有说到,Gradle通过将一系列的Task构建成一个有向无环图,来执行最终的任务,既然是有向无环,那么说明Task之间是有依赖和顺序关系的。那么Task之间的依赖关系如何定义呢?这里就来介绍一下。

       doLast{
       }
       doFirst{
       }
    

    首先看下Task内部的一个API,用于表示Task内部的执行方式

    • doFirst:task执行时,最开始的操作
    • doLast:task执行时,最后的操作
      接着来看下Task之间的顺序如何控制。
    1.dependsOn

    最直接的一个任务依赖另一个任务的执行就是通过denpendsOn方法。

    task task1 << {
        println "我是task1----"
    }
    
    task task2 << {
        println "我是task2----"
    }
    
    //task2 依赖 task1, 执行task2之前先执行task1
    task2.dependsOn task1
    

    执行task2gradlew task2
    结果

    我是task1----
    我是task2----

    mustRunAfter

    当一种场景TaskA依赖TaskB和TaskC,但我们想控制TaskB和TaskC的关系,那么这时候我们如何处理,可能有刚才的介绍我们会用taskB.dependsOn TaskC,但是这样其实就有问题另,因为实际上TaskB是不依赖TaskC的,只是在有TaskA的情况下,我们希望TaskC先执行。这时候就需要用到mustRunAfter

    task taskA {
        doLast{
        println '我是taskA'
        }
    }
    
    task taskB {
        doLast{
        println '我是taskB'
        }
    }
    
    task taskC {
        doLast{
        println '我是taskC'
        }
    }
    
    taskA.dependsOn taskB
    taskA.dependsOn taskC
    taskB.mustRunAfter taskC
    

    执行TaskA,会得到结果

    Task :taskC
    我是taskC

    Task :taskB
    我是taskB

    Task :taskA
    我是taskA

    所以mustRunAfter并不会添加依赖,只是高度Gradle执行的优先级,如果两个Task同时存在,那么就会按照这个定义的优先级执行。

    finalizedBy

    我们如果希望任务执行结束的时候自动执行某个任务,比如我们在打包结束后自动上报包体积到后台,如果用dependsOn,那么我们就需要类似打包任务.dependsOn taskUpload,后续打包就需要通过taskUpload来执行,这样就很别扭,所以这里就有来finalizedBy,用于任务执行结束后自动执行其他任务。

    task taskC {
        doLast{
        println '我是taskC'
        }
    }
    
    task taskD {
        doLast{
        println '我是taskD'
        }
    }
    
    taskC.finalizedBy taskD
    

    执行taskC,会得到结果

    Task :taskC
    我是taskC

    Task :taskD
    我是taskD

    常用Task

    其实我们打开Studio右边的gradle面板就会看到很多的项目,里面的每一项其实就是一个Task。我们也可以用一个简单的方法来看下一个项目打包的时候会执行的所有的Task,前面介绍到了我们会构建一个Task的有向无环图,所以我们可以等这个有向无环图构建成功的时候,将所有的task打印一下

    gradle.taskGraph.beforeTask { Task task ->
        println "executing:  $task.name"
    }
    

    然后在命令行执行./gradlew assemble | grep 'executing',具体命令后面会有个博客介绍一下,这里先简答说下,assemble也是一个Task,我们在Studio的操作面板中执行打包编译实质就是执行这个task,所有这里就相当于执行下这个打包的Task,然后用grep过滤下我们刚才打印的关键词

    executing:  preBuild
    executing:  preDebugBuild
    executing:  compileDebugAidl
    executing:  checkDebugManifest
    executing:  compileDebugRenderscript
    executing:  generateDebugBuildConfig
    executing:  mainApkListPersistenceDebug
    executing:  generateDebugResValues
    executing:  generateDebugResources
    executing:  createDebugCompatibleScreenManifests
    executing:  processDebugManifest
    executing:  mergeDebugResources
    executing:  processDebugResources
    executing:  kaptGenerateStubsDebugKotlin
    executing:  kaptDebugKotlin
    executing:  compileDebugKotlin
    executing:  mergeDebugShaders
    executing:  compileDebugShaders
    executing:  generateDebugAssets
    executing:  mergeDebugAssets
    executing:  javaPreCompileDebug
    executing:  compileDebugJavaWithJavac
    executing:  compileDebugSources
    executing:  processDebugJavaRes
    executing:  checkDebugDuplicateClasses
    executing:  mergeDebugJavaResource
    executing:  transformClassesWithDexBuilderForDebug
    executing:  validateSigningDebug
    executing:  signingConfigWriterDebug
    executing:  mergeDebugJniLibFolders
    executing:  extractProguardFiles
    executing:  preReleaseBuild
    executing:  compileReleaseAidl
    executing:  compileReleaseRenderscript
    executing:  checkReleaseManifest
    executing:  generateReleaseBuildConfig
    executing:  mainApkListPersistenceRelease
    executing:  generateReleaseResValues
    executing:  generateReleaseResources
    executing:  mergeReleaseResources
    executing:  createReleaseCompatibleScreenManifests
    executing:  processReleaseManifest
    executing:  processReleaseResources
    executing:  kaptGenerateStubsReleaseKotlin
    executing:  kaptReleaseKotlin
    executing:  compileReleaseKotlin
    executing:  javaPreCompileRelease
    executing:  compileReleaseJavaWithJavac
    executing:  compileReleaseSources
    executing:  mergeDebugNativeLibs
    executing:  stripDebugDebugSymbols
    executing:  prepareLintJar
    executing:  lintVitalRelease
    executing:  mergeReleaseShaders
    executing:  compileReleaseShaders
    executing:  generateReleaseAssets
    executing:  mergeReleaseAssets
    executing:  signingConfigWriterRelease
    executing:  mergeReleaseJniLibFolders
    executing:  mergeReleaseNativeLibs
    executing:  stripReleaseDebugSymbols
    executing:  mergeReleaseGeneratedProguardFiles
    executing:  processReleaseJavaRes
    executing:  mergeReleaseJavaResource
    executing:  transformClassesAndResourcesWithR8ForRelease
    executing:  transformClassesAndDexWithShrinkResForRelease
    executing:  packageRelease
    executing:  assembleRelease
    executing:  mergeExtDexDebug
    executing:  mergeDexDebug
    executing:  packageDebug
    executing:  assembleDebug
    executing:  assemble
    

    后续的介绍也会围绕这个进行介绍,所以这里我先介绍下我认为平时开发中接触到比较多的几个Task吧

    Task名称 作用
    clean 清除缓存,懂的都懂吧~经常用到
    assemble 打包任务
    install 安装任务,会安装我们打出的包到手机

    插件

    其实我们了解到现在会发现,其实Gradle的核心就是一个包含丰富语法糖,支持丰富DSL的流程控制框架,而框架的内部其实是没有实质的操作的,所以Gradle的构建便捷其实都是由插件提供支持的。插件可以看作是一系列Task的集合,插件添加了新的任务,然后Gradle按照有向无环图进行顺序执行。在Gradle中插件一般分为两种:

    • 脚本插件
      是额外的构建脚本,他会进一步配置构建,我们可以理解我们将部分的配置抽取成一个脚本,然后进行依赖。脚本插件通常可以从本地文件或者远程获取,如果是从本地文件获取则是相对于项目路径,如果是远程获取,则是由HTTP进行指定。
      脚本插件其实并不能是一个真正的插件,他是脚本模块化的基础,所以我们可以把复杂的脚本文件,进行拆分,分段,拆分成一个职责分明的脚本插件。
    • 二进制插件
      是实现了Plugin接口的类,并且采取编程的方式来操作构建。可以理解我们自定义一些需要在编译执行的时候来进行一些自定义处理。

    插件需要用过Project.apply()的方法声明应用,相同的插件可以应用多次。

    //脚本插件
    apply from 'utils.gradle'
    //二进制插件
    apply from 'java'
    

    插件还可以使用插件ID,插件ID作为插件的唯一标示,我们可以注册的时候给插件定义一个唯一ID,后续讲解插件开发的时候会单独讲解。

    gradle.properties

    这个文件前面有提到这个文件,这个文件我们在创建项目的时候,AndroidStudio会自动生成一个这个文件,这个文件是用来配置项目级别的Gradle配置,也就是我们可以在这个文件里配置项目级别的公共属性。
    当然这个文件是可以有多个的,其中子项目的gradle.properties会覆盖rootProject的gradle.properties,但是子项目的的gradle.properties属性只会在子项目中可见,只有rootProject的gradle.properties的属性是全局可见的。

    //gradle.properties
    COMPILE_SDK_VERSION=28
    MIN_SDK_VERSION=15
    
    //setting.gradle
    // 输出Gradle对象的一些信息
    def printGradleInfo(){
        println "COMPILE_SDK_VERSION:" + COMPILE_SDK_VERSION
    }
    
    printGradleInfo()
    

    依赖

    gralde之所以能代替Maven,其依赖管理是一大关键因素。这里先简单介绍下依赖的概念,后面会专门开一篇博客讲解依赖的相关内容。
    首先,Gradle做为一个项目构建的DSL工程,Gradle需要知道项目构建时需要的一些文件,而这些文件往往除了我们自己编写的项目文件,往往还会用到其他工程的一些文件,例如三方的开源库,自己编写的子项目,这些文件就是项目的依赖,Gradle在项目构建的时候需要告诉它项目的依赖是什么,在哪里能够找到他们,然后帮你加入到构建中,有的可能是在本地,有的可能需要到远端下载,有的甚至是另一个工程。我们先简单的看一个依赖。

    apply plugin: 'java'
    repositories {
        mavenCentral()
        google()
        jcenter()
    }
    dependencies {
      implementation 'androidx.appcompat:appcompat:1.0.2'
    }
    

    首先我们在dependencies中声明来我们需要的依赖库,也就是androidx.appcompat:appcompat:1.0.2,这里往往是group:name:version三段式组成,然后gradle在编译的时候知道我们项目的构建需要这个库的依赖支持,那么肯定需要找到这个库,这时候就会去repositories中寻找,这里看到我们声明了三个地址分别是maven、google、jcenter仓库。这样在构建的时候就会从这个仓库地址中下载这个依赖库,然后进行构建。

    总结

    这篇博客可能稍微讲的有点乱,Gradle是一个庞大的体系,里面的每一个点都可以展开讲解,细枝末节很多,所以这篇博客还是篇前期的基础概念讲解,后面会针对这里面的接触概念进行专项拓展讲解。希望自己的这篇Gradle系列能帮助大家把Gradle弄懂吃透。

    相关文章

      网友评论

        本文标题:【Gradle深入浅出】——Gradle基础概念

        本文链接:https://www.haomeiwen.com/subject/sqmlwktx.html