Gradle 之 Android 中的应用

在上一篇文章中 Gradle 之语言基础 Groovy 主要介绍了 Groovy 的基础语法(如果没有 Groovy 的基础,建议先看看上篇文章,如果可以动手敲一下里面的示例代码就更好不过了),也是为本篇文章打基础的。

本篇文章主要介绍 Gradle 在 Android 中的应用(Android DSL 和 Gradle DSL),也是通过一些示例来介绍和理解,主要分为以下一些内容,示例代码都在 GradleForAndroid

一. Gradle 构建生命周期

一个 Gradle 的构建通常有如下三个阶段

  • 初始化:项目 Project 实例会在该阶段被创建。如果一个项目中包含有多个模块,并且每一个模块都有其对应的 build.gradle 文件,就会为每一个模块都创建一个对应的 Project 实例
  • 配置:执行各个模块下的 build.gradle 脚本,为 Project实例创建和配置 Task,构造 Task 任务依赖关系图以便在执行阶段按照依赖关系执行 Task
  • 执行:在这个阶段将会决定执行哪个 Task,哪个 Task 被执行取决于开始该次构建的参数配置和该 Gradle 文件的当前目录

在创建完成一个新的 Android 应用项目之后,一般情况下, .gradle 文件的目录结构如下所示:

GradleForAndroid
      |---- build.gradle
      |---- setting.gradle
      \---- app
              \---- build.gradle

其中,两个文件 build.gradlesetting.gradle 位于项目的根目录下,还有一个 build.gradle 位于 \app\ 目录下。\build.gradle 是顶层构建文件,\app\build.gradle 是模块构建文件。
我们以上面这个新创建的项目来学习 Gradle 的构建生命周期

1.1 初始化

  1. 在初始化阶段,会创建一个 Setting 对象,对应着 setting.gradle 文件,
    Setting 对象的一个主要作用就是声明哪些模块将会参与到构建中去,Setting 文档(Gradle API 5.0)

  2. 在新建的项目中,setting.gradle 文件一般会默认包含一行内容,如下所示

    include ':app'
    

    上面这一行,其实是一行 groovy 代码的简写,对应的是 Setting#include(String[] projectPaths) 方法,表示 :app 模块将会参与到构建中去。
    如果我们创建一个 library 库,setting.gradle 将会变为如下所示,表示 :app:library 两个模块将会参与到构建中

    include ':app', ':library'
    
  3. setting.gradle 脚本文件可以中读取一些只可读的配置信息,这些配置信息的来源可以有如下三个:

    • 可以在本工程的 gradle.properties 文件中定义配置信息
    • 也可以在系统的 gradle.properties 文件中定义配置信息,系统的 gradle.properties 位于 user's .gradle 目录下
    • 还可以通过 -P 命令参数指定配置信息,比如 ./gradlew clean -P cmd='Hello from commandLine' 便在执行 clean task 的时候,指定了 cmd='Hello from commandLine' 配置信息
  4. 上面讲到,一个 setting.gradle 文件对应着一个 Setting 对象,Setting 对象包含的方法如下图所示

    Setting.png

    例如有如下代码

    include ':app', ':library'
    
    println propertiesFile
    println DEFAULT_SETTINGS_FILE
    println getRootProject().name
    println getRootProject().path
    
    getGradle().addBuildListener(new BuildListener() {
        @Override
        void buildStarted(Gradle gradle) {
            println 'buildStarted'
        }
    
        @Override
        void settingsEvaluated(Settings settings) {
            println "settingsEvaluated"
        }
    
        @Override
        void projectsLoaded(Gradle gradle) {
            println 'projectsLoaded'
        }
    
        @Override
        void projectsEvaluated(Gradle gradle) {
            println 'projectsEvaluated'
        }
    
        @Override
        void buildFinished(BuildResult result) {
            println 'buildFinished'
        }
    })
    

    其输出是:

    Hello from gradle.properties
    settings.gradle
    GradleForAndroid
    :
    settingsEvaluated
    projectsLoaded
    projectsEvaluated
    
    BUILD SUCCESSFUL in 0s
    3 actionable tasks: 3 executed
    buildFinished
    

1.2 配置

  1. 在配置阶段,会执行所有的 build.gradle,包括项目根目录下的 build.gradle 和各个 module 下的 build.gradle
  2. 在执行 build.gradle 的时候,会为每个 build.gradle 创建一个对应的 Project 对象,Project 文档(Gradle API 5.0)
  3. 配置阶段会执行 build.gradle 里面的所有代码和 Task 里面的配置代码,比如下面的 printProperties Task,只执行了 doLast{} 之外的代码,doLast{} 之外的代码是 Task 的配置代码
  4. 配置执行完成之后,会根据各个 Task 的依赖关系生成一个有向无环图,可以通过Gradle对象的getTaskGraph方法访问,对应的类为TaskExecutionGraph
  5. 在执行所有的 Gradle Task 之前,都会执行 初始化阶段配置阶段 的代码
  6. 比如有如下代码
    // Top-level build file where you can add configuration options common to all sub-projects/modules.
    println "/build.gradle 开始配置"
    buildscript {
        println "/build.gradle buildscript 开始配置"
        ext.kotlin_version = '1.2.71'
        repositories {
            google()
            jcenter()
        }
        dependencies {
            classpath 'com.android.tools.build:gradle:3.0.1'
            classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    
            // NOTE: Do not place your application dependencies here; they belong
            // in the individual module build.gradle files
        }
        println "/build.gradle buildscript 结束配置"
    }
    
    allprojects {
        println "/build.gradle allprojects 开始配置"
        repositories {
            google()
            jcenter()
        }
        println "/build.gradle allprojects 结束配置"
    }  
    
    task clean(type: Delete) {
        delete rootProject.buildDir
    }
    
    ext {
        local = 'Hello from build.gradle'
    }
    
    task printProperties {
        println "/build.gradle task printProperties 开始配置"
        println '/build.gradle task printProperties'
        println "/build.gradle task printProperties 结束配置"
        doLast {
            println local
            println propertiesFile
            if (project.hasProperty('cmd')) {
                println cmd
            }
        }
    }
    println "/build.gradle 结束配置"
    
    在 Terminal 里面执行 ./gradlew clean 会有如下输出
    Hello from gradle.properties
    settings.gradle
    GradleForAndroid
    :
    settingsEvaluated
    projectsLoaded
    
    // 配置阶段,执行 /build.gradle 里面的代码
    > Configure project :
    /build.gradle buildscript 开始配置
    /build.gradle buildscript 结束配置
    /build.gradle 开始配置
    /build.gradle allprojects 开始配置
    /build.gradle allprojects 结束配置
    /build.gradle allprojects 开始配置
    /build.gradle allprojects 结束配置
    /build.gradle allprojects 开始配置
    /build.gradle allprojects 结束配置
    /build.gradle task printProperties 开始配置
    /build.gradle task printProperties
    /build.gradle task printProperties 结束配置
    /build.gradle 结束配置
    
    projectsEvaluated
    
    BUILD SUCCESSFUL in 0s
    3 actionable tasks: 3 executed
    buildFinished
    

1.3 执行

执行阶段就是指执行某个具体的任务 Task。说道 Task,我想大家应该比较熟悉,在 Android 项目中依赖了 Android Gradle 插件以后,会有许多自带的 Task,比如常用的 cleanassemble 等,而且大家更应该掌握的是如何自定义 Task,关于 Task 会单独抽一节来讲述。

二. 自定义 Task

  1. 任务 Task 代表了在构建过程中的一个单原子性的动作,比如:编程生成 .class 文件或者生成 javadoc 等.
  2. 每一个 task 都是属于某一个 Project 对象的,每一个 task 都有自己的名字,在 projecttask 的名字是唯一的,如果在整个项目 projects 范围内需要指定某个 task 的话,也需要指定 project 的名字,projecttask 的名字中间使用 : 相连接,比如:./gradlew :app:clean
  3. 创建 Task 对象的方法有以下几种
    // 通过 TaskContainer.create(String) 创建 `Task`
    getTasks().create('helloTask') {
        doLast {
            println 'create Task by TaskContainer'
        }
    }
    
    task helloTask1 {
        doLast {
            println 'create Task by task(String name)'
        }
    }
    
    class HelloTask extends DefaultTask {
        def message = 'create Task by extends DefaultTask'
    
        @TaskAction
        def hello() {
            println message
        }
    }
    // 通过 type 参数可以指定该 task 的父类,默认的父类是 DefaultTask
    task helloTask2(type: HelloTask)
    
  4. 一个 Task 是由一系列的 Action 组成的,当一个 Task 执行的时候就是按照一定的顺序执行这些 Action,可以有以下两种方式向 Task 中添加 Action
    • 通过闭包 Closure 的方式添加 Action
      class HelloTask extends DefaultTask {
          def message = 'create Task by extends DefaultTask'
      
          @TaskAction
          def hello() {
              println message
           }
      }
      
      task helloTask2(type: HelloTask) {
          doFirst {
              println 'helloTask2 doFirst'
          }
      
          doLast {
              println 'helloTask2 doLast'
          }
      }
      
    • 直接添加 Action 实例对象
      def taskDef = task helloTask3(type: HelloTask)
      taskDef.doFirst(new Action<Task>() {
          @Override
          void execute(Task task) {
              println 'helloTask3 Action execute doFirst'
          }
      })
      taskDef.doLast(new Action<Task>() {
          @Override
          void execute(Task task) {
              println 'helloTask3 Action execute doLast'
          }
      })
      
  5. Task 依赖关系 & Task 执行顺序
    Task 中有两个很重要的概念 dependsOnmustRunAfterdependsOn 用于声明两个 Task 对象之间的依赖关系,mustRunAfter 用于声明一个 Task 必须在另一个 Task 之后运行,虽然感觉差不多,但是实际上还是有区别的
    • helloTaskB.dependsOn(helloTaskA) 中,可以单独运行 helloTaskA,但是运行 helloTaskB 的时候会触发 helloTaskA 的执行
    • helloTaskC.mustRunAfter(helloTaskA)中,helloTaskAhelloTaskC 都是可以单独运行的,但是当 helloTaskChelloTaskA 同时运行时,helloTaskC 一定会在 helloTaskA 之后运行
    task helloTaskA {
        doFirst {
            println 'helloTaskA doFirst'
        }
    }
    
    task helloTaskB {
        doFirst {
            println 'helloTaskB doFirst'
        }
    }
    
    helloTaskB.dependsOn(helloTaskA)
    
    task helloTaskC {
        doFirst {
            println 'helloTaskC doFirst'
        }
    }
    
    helloTaskC.mustRunAfter(helloTaskA)
    
  6. Android 中使用自定义 Task
    在 Gradle 构建的时候,需要将自定义的 Task 添加到构建过程中时,需要把握好添加自定义 Task 的时机与位置
    • 下面一幅图清晰地展示了 Gradle 构建过程中一些关键的回调,可以在下面一些回调中添加自定义 Task
    • project 对象中,可以通过 gradle 对象得到 TaskExecutionGraph 的实例对象,也可以通过 TaskExecutionGraph 实例对象一些关键回调添加自定义的 Task。比如下面这个例子,就是在 TaskExecutionGraph 实例对象准备好之后,弹出一个 dialog 用于输入 storePasskeyPass,然后将 storePasskeyPass 设置到 android.signingConfigs
    apply plugin: 'com.android.application'
    import groovy.swing.SwingBuilder
    
    //......
    
    gradle.taskGraph.whenReady { taskGraph ->
        if (taskGraph.hasTask(':app:assembleRelease')) {
            def storePass = ''
            def keyPass = ''
            if (System.console() == null) {
                System.setProperty('java.awt.headless', 'false')
                new SwingBuilder().edt {
                    dialog(modal: true, title: 'Enter password', alwaysOnTop: true, resizable: false, locationRelativeTo: null, pack: true, show: true) {
                        vbox { // Put everything below each other
                            label(text: "Please enter store passphrase:")
                            def input1 = passwordField()
                            label(text: "Please enter key passphrase:")
                            def input2 = passwordField()
                            button(defaultButton: true, text: 'OK', actionPerformed: {
                                storePass = input1.password;
                                keyPass = input2.password;
                                dispose();
                            })
                        }
                    }
                }
            } else {
                storePass = System.console().readPassword("\nPlease enter store passphrase: ")
                keyPass = System.console().readPassword("\nPlease enter key passphrase: ")
            }
    
            if (storePass.size() <= 0 || keyPass.size() <= 0) {
                throw new InvalidUserDataException("You must enter the passwords to proceed.")
            }
    
            storePass = new String(storePass)
            keyPass = new String(keyPass)
    
            android.signingConfigs.release.storePassword = storePass
            android.signingConfigs.release.keyPassword = keyPass
        }
    }
    

如下图所示 TaskExecutionGraph 的方法结构如下图所示,都是非常实用方便的方法

TaskExecutionGraph.png

三. Android DSL & Gradle DSL

3.1 Android DSL

Android DSL 是 Gradle 的一个 Android 插件,其实在使用 Android Studio 开发的时候经常会和 Android DSL 打交道,比如下面 android{ } 闭包 里面的内容都是 Android DSL

apply plugin: 'com.android.application'
import groovy.swing.SwingBuilder

android {
    compileSdkVersion 27
    defaultConfig {
        applicationId "com.lijiankun24.gradleforandroid"
        minSdkVersion 15
        targetSdkVersion 27
        // ......
    }
    buildTypes {
        // ......
    }
    signingConfigs {
        // ......
    }
}

至于里面都有哪些 API,可以去 Android DSL 文档 查看,也可以去 GitHub 上面搜 android-gradle-dsl

3.2 Gradle DSL

Gradle DSL 在上面介绍 Gradle 生命周期和自定义 Task 的时候已经介绍过了,比如上面介绍的 SettingTaskExecutionGraph 都是 Gradle DSL 中的类,其他的类和方法等 API 可以去 Gradle DSL 文档 查看,或者可以在 Android Studio 中像查看 Android SDK 源码一样去查看 Gradle DSL 的源码

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

推荐阅读更多精彩内容