美文网首页
Just Enough Gradle for Android

Just Enough Gradle for Android

作者: 珞泽珈群 | 来源:发表于2018-05-24 16:32 被阅读33次

    前言

    做Android开发的没有不知道Gradle的。时不常地去build.gradle中添加一些dependencies,或者修改一些诸如versionCoce,versionName之类的属性都已经是家常便饭。然而基本上也就仅限于此。我们都知道,Gradle是基于Groovy语言的,得益于Groovy语言的简洁与直白,应付所谓的“家常便饭”根本不需要额外的学习。然而,作为一名优秀的“程序猿”,我时常好奇,Gradle到底是啥,如此简洁的背后是不是隐藏着什么大秘密。于是我决定稍微深入地学习一下Gradle。

    先抛出结论,经过我这段时间的学习,对Gradle背后的秘密有了一定的了解。然而,理解了这些之后,我对Gradle的使用仍然没有超过所谓“家常便饭”的级别。如果你秉持着能用就好的原则,那么现在你就可以把这篇文章关掉了。余下的内容只是对Gradle的释惑,让我们在使用Gradle时不再只是“能用”,而是“够用”。不过也仅仅是够用。Just Enough Gradle for Android...

    Just Enough Groovy for Gradle

    众所周知,Gradle是使用Groovy语言编写的(不过现在也有Kotlin版本的),我们先学习一点Groovy的基本语法。

    //变量定义
    def x = 3
    //也可以传统点
    int x = 3
    
    //函数定义
    String testFunction(def arg1) {//无需指定参数类型,当然指定也可以
        return ""
    }
    
    //无类型的函数定义,必须使用 def 关键字
    def nonReturnTypeFunc() {
        last_line //return可以省略,如果没有return,最后一行代码的执行结果就是本函数的返回值
    }
    //也可以传统点
    String getString() {
        return "I am a string"
    }
    
    //函数调用可以不加括号
    println("Hello, Groovy!")
    println "Hello, Groovy!"
    //没有参数的函数,还是要加上()的,不然谁知道是函数还是变量,例如上方的getString函数调用
    getString()
    
    //字符串,单引号的字符串完全是字面值;双引号字符串,有字符串插入
    String name = 'Groovy'
    assert "Hello, ${name}!" == 'Hello, Groovy!'
    //在不引起混淆的时候可以简写
    assert "Hello, $name!" == 'Hello, Groovy!'
    
    //List定义
    def nums = [1, 2, 3]
    assert nums[0] == 1
    assert nums[-1] == 3 //倒数第一个
    //Map定义
    def map = [a:1, b:2, c:3]
    assert map.a == 1
    assert map['b'] == 2
    assert map.get('c') == 3
    //List、Map上都定义了诸如each,all,find等函数
    nums.each {
        //隐含一个参数 it
        println it
    }
    
    //闭包作为函数的最后一个参数,可以写到括号外面
    map.each ({
        println "key=${it.key},value=${it.value}"
    })
    //等价于
    map.each() {
        println "key=${it.key},value=${it.value}"
    }
    //括号省略
    map.each {
        println "key=${it.key},value=${it.value}"
    }
    
    
    //闭包closure,类似于Java 8和Kotlin中的lambda表达式,不同的是在Groovy中闭包都属于一个叫Closure的类
    //这就有个问题,如何知道当前闭包的参数有几个,类型是啥,这就有赖于你对API的熟悉程度
    public static <T> List<T> each(List<T> self, Closure closure)
    
    

    Gradle概览

    问:码农最重要的能力是什么?答:打码,噢,不对,码代码。但是除了码代码之外,每个码农还必须会把代码部署完成。对于Android来说就是打包。而Gradle就是帮我们做这一系列事情的工具,也就是构建工具。

    在Android Studio中一个总工程称为一个Project,每个Project可以包含一个或多个模块,称之为Module,比如说最常见的app Module。在Gradle中,每个build.gradle文件都会生成一个Project对象,顶层build.gradle生成的Project称为RootProject,各个module中的build.gradle生成的Project的称为SubProject。

    Gradle中的每一个Project都包含一系列Task,一个具体的编译过程是由一个个的 Task 来定义和执行的。比如 Android APP Project 包含源码编译Task、资源编译Task、lint检查Task、测试Task、打包Task、签名Task等等。一个Project包含有多少Task,主要是由插件决定的。而插件就是用来定义Task的。例如编译Java有Java插件,编译Android APP 有 Android Appliaction 插件,编译 Android Library 有 Android Library 插件。

    //app的 build.gradle 指定了android application插件
    apply plugin: 'com.android.application'
    
    //module的 build.gradle 指定了android library插件
    apply plugin: 'com.android.library'
    
    //纯java工程的 build.gradle 指定了java插件
    apply plugin: 'java'
    

    每一个build.gradle对应了一个Project,这个Project包含了哪些Task主要由我们指定的插件决定。

    至此,我们知道,编译过程即Task执行过程。不过还有个问题,以Android APP的编译为例,打包Task不是孤立的,它必须依赖于诸如源码编译Task等的执行,也就是说必须先执行源码编译Task等一系列Task,才能执行打包Task。这就是Task之间的依赖。每个Project中的Task会在配置阶段,根据彼此之间的依赖关系构成一个有向无环图(DAG)。这个DAG决定了Task的执行顺序。下图展示了Java插件的Tasks之间的依赖关系(本来想找Android 插件的Tasks,但是没有找到,可能是因为Android很多Task是动态生成的,Tasks不确定):

    JavaPluginTasks

    如上图,箭头方向即代表了依赖关系。例如 build task 依赖于 check task 和 assemble task,也就是说必须先执行check 和 assemble,才能执行build。以此类推。例如我们执行build task

    > gradlew build
    :compileJava UP-TO-DATE
    :processResources UP-TO-DATE
    :classes UP-TO-DATE
    :jar
    :assemble
    :compileTestJava UP-TO-DATE
    :processTestResources UP-TO-DATE
    :testClasses UP-TO-DATE
    :test UP-TO-DATE
    :check UP-TO-DATE
    :build
    BUILD SUCCESSFUL
    Total time: 1.956 secs
    

    可以看出依赖链上的所有task被依次执行。定义task之间的依赖关系也很多简单:

    //定义两个task,hello 和 world,后面会有Task的更详细的介绍
    task hello << {
        println 'hello'
    }
    task world << {
        println 'world'
    }
    
    world.depensOn hello
    
    //或者task定义的时候就指定
    task world(dependsOn: hello) << {
        println 'world'
    }
    

    Gradle深入

    Gradle工作流程

    Gradle 工作包含三个阶段:

    1. 初始化阶段。读取gradle.properties中的参数;执行settings.gradle,确定共有多少个Project。
    2. 配置阶段。解析每个 project 中的 build.gradle,根据Taks之间的依赖关系构建有向无环图,确定Task的执行顺序。
    3. 执行阶段。

    如图所示,在每个阶段结束之后,我们都可以添加一些Hook,完成我们的一些目的。

    Gradle 主要有三种对象,这三种对象和三种不同的脚本文件对应。

    1. Gradle 对象:在整个执行过程中,只有这么一个对象。Gradle对象的数据类型就是 Gradle。我们一般很少去配置这个对象。
    2. Project 对象:每一个 build.gradle 会转换成一个 Project 对象。
    3. Settings 对象:settings.gradle 会转换成一个 Settings 对象。

    其中最重要的是Project对象。build.gradle 是每个Project构建的入口,在这个文件我们主要:

    • 加载插件
    • 设置属性、配置插件(例如,版本号,依赖配置等等)
    • 自定义Task(独立的,或者依赖于别的Task)
    • 改变已有Task的行为(一般是增加一些我们自定义的行为)

    一个常见的问题是,我们的工程往往不止一个build.gradle 文件,也即不止一个Project对象,怎么能让这些对象“共享”一些属性呢?Gradle 提供了一种名为 extra property(额外属性) 的方式。extra property 支持Project 和 Gradle 对象。

    例如在Android Studio中创建一个支持Kotlin的项目,则其顶层的 build.gradle 如下:

    buildscript {
        //额外属性
        ext.kotlin_version = '1.2.41'
        //也可以这样写
        ext {
            kotlin_version = '1.2.41'
            //更多的额外属性
            ...
        }
        
        repositories {
            google()
            jcenter()
        }
        
        dependencies {
            classpath 'com.android.tools.build:gradle:3.1.2'
            classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
        }
    }
    
    allprojects {
        repositories {
            google()
            jcenter()
        }
    }
    

    以如上这种方式定义额外属性,是在RootProject对象上定义了额外属性,而所有的SubProject都会“继承”RootProject的额外属性。这样就实现了在多个Project对象“共享”属性。

    真正有用的知识

    上面扯了那么多,不禁让人疑问,有毛用?!确实是用处不大。看来,是时候展现真正的技术了。

    先来一波总结:

    Gradle流程

    很明显,关隘就在于Task。前面说过Task主要是由插件定义的,当然我们也可以定义自己的Task,完成我们的目的。对于Gradle的使用,主要在两个方面,首先是对插件定义好的属性地配置,这就要求我们了解插件定义了哪些属性,这些属性起什么作用;其次是自定义Task,完成我们特定的目的。

    定义Task的几种方式

    task hello {
        doFirst {
            println 'hello'
        }
        doLast {
            println 'world'
        }
    }
    
    task('hello') {
        doFirst {
            println 'hello'
        }
        doLast {
            println 'world'
        }
    }
    
    //tasks是project中的一个属性
    tasks.create('hello') {
        doFirst {
            println 'hello'
        }
        doLast {
            println 'world'
        }
    }
    
    //没啥用的task
    task hello {
        println 'hello'
    }
    
    

    前面说过,Gradle有三个阶段初始化、配置和执行。如果我们像最后一种方式定义一个Task的话,那么闭包中的代码将在配置的时候执行,这没啥卵用,也不是我们定义Task的目的。
    当我们执行一个Task的时候,其实是执行其拥有的actions列表,这个列表保存在Task对象实例中的actions成员变量中
    private List<ContextAwareTaskAction> actions = new ArrayList<>()
    所以说,Task中的doFirst和doLast方法只是向actions中添加Action,这样才能在执行阶段执行。

    task hello {
        //doFirst并不常用
        doLast {
            println 'world'
        }
    }
    
    //可以简写, << 运算符的重载,doLast 的简写
    task hello << {//这里即是一个Action的闭包
        println 'world'
    }
    
    //不必从头定义每个task,Gradle已经帮我们定义好了很多常用的Task,我们只需要配置一下就能用了
    //如下是一个删除task,要删除的是根目录下的build目录
    task clean(type: Delete) {
        delete rootProject.buildDir
    }
    
    //或者一个拷贝task
    task copyOutputs(type: Copy) {
        from "$buildDir/outputs/apk"
        into '../results'
    }
    

    groovy 支持运算符重载,Task类上 << 的重载就是一个很好的例子。选择这个运算符进行重载还挺形象的,由左移引申为向actions列表末尾添加。

    小结

    至此,我们基本上对Gradle有了一个大致的了解。Gradle是一个构建工具,利用插件帮我们定义了许多Task,执行这些Task(其实是Task中的Action)完成构建任务。我们需要做的就是:首先,配置插件的各种属性,方便、快捷,能满足我们大部分的需求;其次,如果配置属性没有办法满足我们的要求,我们可以自定义Task,达到我们的目的。这篇文章主要是对Gradle知识性地介绍,想看更多Gradle在Android中应用的实例,请看Android Gradle 多维度实例

    参考书籍

    Gradle for Android
    Gradle Recipes for Android
    《深入理解Android之Gradle》
    《Android Gradle权威指南》

    相关文章

      网友评论

          本文标题:Just Enough Gradle for Android

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