Gradle 基础的认知和学习套路分享

写在前面


自从用上了Android Studio 进行 Android 开发,不可避免地要与 Gradle 打交道。它颠覆了过去使用 Eclipse + ADT 时代(暴露年龄...)对 Android 构建认知,惊喜于构建效率的改进,同时因对 Gradle 本身的不了解,或者说不知道应该了解啥,导致面对构建出错见红,往往都不知所措、无从下手。
直观印象中:

Gradle 进行构建的效率高、使用方便,对照文档调整参数,修改脚本就可以了。

Gradle 也不容易理解,Android Studio 官方只说了插件怎么用,有哪些能力,但知识点多琐碎,体系脉络稍欠清晰,单纯看着脚本语法例子也不好理解记忆。

正文前言


AS(Android Studio)、AGP(Android Gradle Plugin)、
AP DSL(Android Plugin DSl),是日常研发不离手的东西,它们之间通过Gradle,建立起紧密的联系。

网络上也有不少文章,对 Gradle 构建脚本进行详细讲解,例如:Gradle基础脚本全攻略,也值得细细研读。

写这篇文章的目的,是想分享自己在学习(啃Gradle官方文档)Gradle 过程中,提炼的认知和整理,既方便日后的知识快速回溯,同时也希望能帮助大家对 Gradle 有整体的感性认识,便于自行研读原汁原味的官方文档

全文分主要分两部分:

  1. Gradle 基础知识的理解
    • 以本人理解后的用语,尽量通俗。
    • 不求内容全面,但求向读者介绍主要内容,对阅读理解官方文档的内容有所帮助。
  2. 在学习、使用 Gradle 过程中,解决问题的一些套路和思维。

Gradle基础知识


我参考阅读的文档版本为 Gradle Docs V4.1。正如前面所说,跟人感觉官方文档的内容编排是单纯地从使用角度,循序渐进地来展开。如果把这个文档看成一个人,她应该在说:

首先,你要安装下载安装 Gradle,基本的使用方式、命令行、Wapper 很好很强大强推你用、多项目怎么构建、Bla Bla Bla ...
非常棒,现在你对 Gradle 有了**感性认识**。然后,要介绍怎样写构建脚本,这是清单列表:
- Build Script 的基本概念
- 项目和任务、脚本怎样初始化
- 聊聊任务、怎样处理文件、你可以直接用 Ant
- 构建其实有生命周期的
- 日志功能我们也有
- 依赖管理、多项目构建;
- 其实我们还有 A插件、B插件、C插件、Bla Bla Bla插件...

你想自定义构建?当然可以,重要事情说多遍:插件、插件、插件 ...

私以为,把 Wrapper 编排在靠前位置,对知识体系初期理解和建立,不太很友好。倒不如从“生命周期”开始说起,会比较好理解。

1.Gradle运行的生命周期

Initialization(初始化) Configuration(配置) Execution(执行)
初始化配置、系统版本、环境变量版本检查、Gradle 版本检查,Gradle 类路径载入 执行相关 .gradle 脚本,确定要执行的项目和任务,并建立可执行的不可循环有向图 根据有向图的顺序,执行任务
1.1 Initialization(初始化)阶段

这个阶段有 90% 的工作跟 Gradle 进程启动有关:

  • 检查运行的机器上对应的依赖库、版本、Gradle SDK 路径
    和版本等 Gradle 运行环境的检查工作
  • 加载 Gradle SDK、启动 Daemon 进程,正式开始干活

另外的10%,是在启动的 Gradle 运行环境中,创建项目对象,

  • 寻找 settings.build 脚本文件,解析所有合法的目录,寻找 build.gradle 脚本文件
  • 创建对应 build.gradle 脚本文件的Project对象
1.2 Configuration(配置)阶段

各个 Project 对象都已经初始化完毕(虽然文档用词是 configured,个人认为用“初始化”比较贴切),这个阶段开始执行对应的构建脚本 build.gradle 中脚本代码。

  • 执行 allprojects{}subprojects{} 等脚本块的配置设置
  • 创建项目的所有任务 Task 对象,分析建立本次构建要执行的任务有向图。

简单概括:配置 Project、创建 Tasks、创建 Tasks 有向图 DAG。

1.3 Execution(执行)阶段

根据有向图的任务依赖顺序,执行 Tasks。
看似简单地一句话带过,但实际上在整个 Gradle 运行中,干体力活最多的节点。

关于“生命周期”的概念,相信看官们都容易理解,官方文档也写的清晰,下面在“生命周期”层往深处进一步看看。

2.脚本内容及代理对象的关系映射的理解

以下是一个常见的项目构建脚本 build.gradle 文件。

// 配置
buildscript {
    repositories {...}    // 定义构建脚本依赖的库仓库
    dependencies {...}    // 定义构建脚本的依赖
}

allprojects {
    // 闭包隐含参数it,相当于(当前project + subprojects)集合的for循环体
    // 每个project都有,只是在rootProject用得比较多
}

subprojects {
    // 闭包隐含参数it,相当于subprojects集合的for循环体
    //每个project都有,只是在rootProject用得较多
}

构建脚本中代码的作用,在Gradle构建语言参考中都能找到明确的说明,但不够详尽。实际上,还有很多 DSL 文档没有列出的API可供使用,需要我们自行去发掘。

首先,列出在 Gradle API 中重要的类:

ProjectTaskAction(Groovy)ClosureScriptHandlerNamedDomainObjectContainerRepositoryHandlerArtifactHandlerDependencyHandlerPluginManager

这些类都是脚本文件中,常见的代理对象类型。关于代理对象、代理设计模式等概念认知,就不在这里阐述。
下图所示为 Gradle 相关 API 类图。为求直观理解,并没有严格遵守 UML 图的规范,请见谅。

Gradle相关API类图.jpg

类图主要展示了 Project 相关的三部分内容:

1.依赖配置相关的内容
2.插件的能力
3.暴露 DSL 相关的依赖类

build.gradle 构建脚本代码片段1:
buildscript {
    repositories {...}    
    dependencies {...}
}

按照官方文档的描述,build.gradle 对应一个 Project 对象,通过方法 getBuildscript()获取 ScriptHandler 对象。

   /**
     * Returns the build script handler for this project. You can use this handler to query details about the build
     * script for this project, and manage the classpath used to compile and execute the project's build script.
     *
     * @return the classpath handler. Never returns null.
     */
    ScriptHandler getBuildscript();

    /**
     * <p>Configures the build script classpath for this project.
     *
     * <p>The given closure is executed against this project's {@link ScriptHandler}. The {@link ScriptHandler} is
     * passed to the closure as the closure's delegate.
     *
     * @param configureClosure the closure to use to configure the build script classpath.
     */
    void buildscript(Closure configureClosure);

ScriptHandler 是脚本块 buildscript{} 的代理对象类型。
DSL中的配置语句,实际被解析为对 ScriptHandler 代理对象的相关属性set方法,该动作是在“配置阶段”执行。至于 DSL 代码是如何被解析的,就不在这篇文章里深入了。

build.gradle 构建脚本代码片段2:
repositories {...} 
dependencies {...}

同理,Project 类还有 getRepositores()getDependencies()方法

    /**
     * Returns a handler to create repositories which are used for retrieving dependencies and uploading artifacts
     * produced by the project.
     *
     * @return the repository handler. Never returns null.
     */
    RepositoryHandler getRepositories();

    /**
     * <p>Configures the repositories for this project.
     *
     * <p>This method executes the given closure against the {@link RepositoryHandler} for this project. The {@link
     * RepositoryHandler} is passed to the closure as the closure's delegate.
     *
     * @param configureClosure the closure to use to configure the repositories.
     */
    void repositories(Closure configureClosure);

/**
     * Returns the dependency handler of this project. The returned dependency handler instance can be used for adding
     * new dependencies. For accessing already declared dependencies, the configurations can be used.
     *
     * <h3>Examples:</h3>
     * See docs for {@link DependencyHandler}
     *
     * @return the dependency handler. Never returns null.
     * @see #getConfigurations()
     */
    DependencyHandler getDependencies();

    /**
     * <p>Configures the dependencies for this project.
     *
     * <p>This method executes the given closure against the {@link DependencyHandler} for this project. The {@link
     * DependencyHandler} is passed to the closure as the closure's delegate.
     *
     * <h3>Examples:</h3>
     * See docs for {@link DependencyHandler}
     *
     * @param configureClosure the closure to use to configure the dependencies.
     */
    void dependencies(Closure configureClosure);
build.gradle 构建脚本代码片段 3:
subprojects {
  repositories {...}    
  dependencies {...}
}

对应 API:

    /**
     * <p>Returns the set containing the subprojects of this project.</p>
     *
     * @return The set of projects.  Returns an empty set if this project has no subprojects.
     */
    Set<Project> getSubprojects();
    /**
     * <p>Configures the sub-projects of this project</p>
     *
     * <p>This method executes the given {@link Action} against the sub-projects of this project.</p>
     *
     * @param action The action to execute.
     */
    void subprojects(Action<? super Project> action);

    /**
     * <p>Configures the sub-projects of this project.</p>
     *
     * <p>This method executes the given closure against each of the sub-projects of this project. The target {@link
     * Project} is passed to the closure as the closure's delegate.</p>
     *
     * @param configureClosure The closure to execute.
     */
    void subprojects(Closure configureClosure);

通过以上三个脚本代码片段及对应的 Gradle API,我们可以理解为:

在 build.gradle 脚本文件中,可以用面向对象的思维去看待脚本代码,并使用。作为开发者,用面向对象的思维去理解东西,应该是比较熟悉的。

在「Gradle生命周期」之中,有两个重要的抽象:项目 Project 和任务 Task。
下图所示为简单的 Gradle 运行流程图。重点感受一下 Project、Task、以及脚本文件在一个生命周期中的情况。


Gradle运行流程图.jpg

前面提到 DSL 的配置代码是在“配置阶段”执行,那么怎样才能在“执行阶段”执行呢?看看以下的例子:

task clean(type: Delete) {
    doFirst {
        println 'do first'
    }
    delete rootProject.buildDir
    println 'do config'
}

task test(type:Delete, dependsOn:clean) {
    doLast {
        println 'do last'
    }
}

执行命令,调用任务test "./gradlew test",输出运行结果:

> Configure project : 
do config

> Task :clean UP-TO-DATE
do first

> Task :test 
do last

BUILD SUCCESSFUL in 1s
2 actionable tasks: 1 executed, 1 up-to-date

因为“执行阶段”只会执行 Task 的内容,所以前面的 DSL 配置内容不会在此阶段被运行。
doFirst 和 doLast 是 Task 的 DSL 语法。常用的插件如: java、com.android.application 等,都是通过Plugin机制,在初始化项目的时候创建了所有的Task对象,能满足大部分的工作需要。
当遇到需要自定义 Task 的情况,如果工作内容不复杂,可以直接在构建脚本中定义。但如果比较复杂,可采取建立 buildSrc 目录模块或者自定义插件类实现,这里就不深入展开了。

学习经验小结


1.较快且有效的阅读文档方法
​ - 重视对目录的咀嚼:用思维导图把目录摘录一遍,最好能在脑海里能列出重要部分章节标题。
​ - 精读并建立自己的索引(逐字逐句翻译琢磨)一遍,不求记住所有,只需要有印象即可。同时,加上“自联想短语”建立一种知识体系的记忆索引,有助于后面的回溯。例如:Gradle 是平台、能力源自插件、项目和任务都是对象、项目对象映射文件夹等等

2.快速查找、确定相关插件 DSL(文档未提及的)可用的属性、参数配置的步骤:
​ - 首先,在 Groovy DSL Reference 文档 ,定位脚本块的代理对象,进而确定对象的类。
​ - 然后,在 Gradle API Javadoc 文档 ,查找该类,进一步查看相关属性或方法。

3.尝试把 build.gradle 脚本文件的内容,对号入座地套进「Gradle 生命周期」执行流程中理解。

结语


要完全掌握 Gradle,不是三言两语能详述,对于刚接触 DSL 概念的同学看官更是抽象不易懂。通过「Gradle 生命周期」作为切入点,把各个零散的知识点串联起来,把疑问逐个解破,也是一种求知的技巧。

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

推荐阅读更多精彩内容