• Gradle 爬坑指南 -- 理解 Plugin、Task、构建过程
  • 上文我们了解了 Plugin 插件、Task 任务、Gradle 3大构建阶段。本文会继续深入,深入源码带大家了解 Gradle 3大内置对象: Gradle、Setting、Project ,然后再了解下 Gradle 给我们提供的 hook 勾子函数

    Gradle 核心模型

    Gradel 核心模型是什么,很多人应该不清楚这个,其实就上前面有提到过的 Gradle 3大内置对象: Gradle、Setting、Project ,看官方文档中的原话:

    我怎么知道 setting script 背后对应的是哪个对象呢? 整个 Gradle 只有三个这样的对象: init script 对应 Gradle, setting script 对应 Settings, build script 对应 Project. 对这三者的关系, 你需要通过 Gradle 生命周期 来解惑

    Gradle 构建工具虽然让我们使用 .gradle 脚本来写构建逻辑,但是在编译阶段还是会把脚本文件编程成 java 对象再执行

  • init.gradle 脚本编译完了会生成 Gradle 对象
  • settings.gradle 脚本编译完了会生成 Setting 对象
  • build.gradle 脚本编译完了会生成 Project 对象
  • Gradle 本质还是个 java 项目,推荐大家都把源码下下来,导入 AS 中看看,其实也不难, Gradle、Setting、Project 在源码中都是接口,实际我们获得到的对象都是实现类,这里我们只看接口定义的函数就能明白很多东西了,具体的实现类应该是引入的插件实现的,比如 android、java 构建插件

    我们再来看看脚本文件中那些令我们头疼的 DSL 代码块,脚本中的 DSL 代码块除了来自插件的,剩下的都来自 Gradle 本身

    app build.gradle -->

    ext.kotlin_version = "1.4.10"
    buildscript {
        ext.kotlin_version = "1.4.10"
        repositories {
            google()
            jcenter()
        dependencies {
            classpath "com.android.tools.build:gradle:4.0.1"
            classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    allprojects {
        repositories {
            google()
            jcenter()
    task clean(type: Delete) {
        delete rootProject.buildDir
    

    脚本中每一个 DSL 都对应 Gradle 核心模型对象中的一个方法,buildscript{...}、repositories{...}、dependencies{...}、allprojects{...}、task{...} 这些都能找到对应的方法,大家且随我一起看看源码,不用多深入,单看接口设计就可以了

    哈哈,看到这里,很多函数是不是很眼熟,是不是就是我们在 build.gradle 脚本中写的 DSL 代码块呀!遇到没见过的 DSL 大家不妨到 Gradle 项目源码中翻一翻,脚本里面能写什么 DSL 大家也可以过来看看源码,ctrl+F12 一下就清楚了,注释写的还可以,AS 有翻译插件,大家要善用翻译看官方的解释

    查看源码的思路其实一开始我也没想到,没想到只要看看接口声明就能搞清楚状况,感谢来自 Gradle 团队的推广视频:来自Gradle开发团队的Gradle入门教程

    官方出品,就是不一样,比博客、文档、其他机构的教学视频有深度多了,还没看的小伙伴强烈建议看一看。开眼界,有点体会到使用源码的味道了,不知道大家 get 到没有 ヾ(≧O≦)〃嗷~

    Hook 函数

    Gradle 的 Hook 函数也有叫生命周期的,就像是我们给 Application 注册监听函数一样,Gradle 允许我们在构建阶段施加自己的影响

    这部分内容很繁杂,我也是看了很多文章,仔细推敲之后才决定从这个角度写这块内容

    从不同角度对 Hook 函数进行分组

    Gradle 提供的 Hook 函数挺多的,有点乱哈,反正我刚看完就是这感觉。这些 Hook 函数之间有不同的设计考虑、角度,基于此我们尝试对这些 Hook 函数进行分组,以方面记忆、理解

    这里我们就要结合上面所讲的 Gradle 核心模型了 ヾ(´∀`o)+ 知识点之间从来都是相互关联的

    1. 从 Gradle 核心模型的角度看:

  • Gradle、Settings 都是全局对象,其提供的 hook 函数自然对整个构建过程起作用
  • Project 对象针对的是单个项目,构建过程中会有多个 Project 对象,自然 Project 对象提供的函数只能对本项目起作用
  • Root Project 对象有些特殊,既有可以对全局进行设置的 hook 函数,也有只对自身起作用的 hook 函数,大家熟悉的 allProject() 就是其中可以对全局进行设置的函数。一般 hook 全局设置都不在根脚本中写,而是选择在 settings 脚本里写
  • 2. 从函数命名的角度看:

  • Evaluated 单词代表构建阶段执行的单个 .gradle 脚本
  • beforeEvaluated 执行该脚本之前做什么
  • afterEvaluated 执行该脚本之后做什么
  • ProjectEvaluated 一个意思,但是 Project 比 Evaluated 含义大一点,Evaluated 专门是指每个 module 中的 .gradle 脚本。而 Project 是指某个 module 的整个构建过程或者可以理解为 Project 对象本身。在 hook 函数看上2者差不多,但还是要理解这2个单词的区别
  • beforeProject 执行该项目之前做什么
  • afterProject 执行该项目之后做什么
  • 3. 从脚本执行的角度看:

  • 我们在 .gradle 脚本中写的 hook 函数,肯定只能在该脚本执行后才能生效,要是在脚本中定义诸如 beforeEvaluated 这样的函数大家觉得有意义吗?此时脚本都执行了,你再定义该脚本执行前应该执行什么就太晚了,应该想办法在该脚本执行前写这个函数
  • 4. 从构建流程的角度看:

  • Initialization 初始化阶段 --> 会执行 init.gradle、settings.gradle 这2个脚本。init.gradle 脚本不推荐使用,因为影响范围太广,谁知道什么时候就会造成未知的困扰。我们都是在 settings.gradle 脚本中写 hook 函数
  • Configuration 编译阶段 --> 子项目脚本中一般只写影响范围只在本项目的 hook 函数。但是要注意 hook 函数执行的流程,比如你在子项目脚本中写 beforeEvaluated 就是纯扯淡。脚本都运行起来了,还写这个 hook 有个啥用。所以要注意 hook 必须写在正确的位置,要结合整个构建流程来考虑 hook 函数的书写位置
  • Execution 执行阶段 --> 这个阶段我们写不了 hook 函数的,就算是能写又能怎么样呢,该怎么执行构建任务,已经在上一个环节都已经决定好了,这个阶段我们施加不了任何影响
  • 5. 从函数设计的角度看:hook 函数分2种

  • 一种是单个功能的 hook 函数,比如 beforeEvaluated,这类 hook 函数有很多
  • 一种是 listener 函数,这类函数往往可以同时实现对个功能,addLisenter() 就是这类 hook 函数,可以添加各种监听。大多数单功能的 hook 函数都可以用 listener 来代替。另外 listener 还可以通过遍历的方式实现对所有的 Project 进行设置、操作
  • 6. 从函数作用的目标角度看:

  • 一种专门监听脚本的执行,比如 beforeEvaluated 就是
  • 一种专门监听项目的执行,比如 beforeProject,gradle.allProject() 可以对所有项目对象进行操作
  • 一种专门监听 task 的执行,比如 gradle.taskGraph.whenReady()
  • Hook 函数中 Gradle 3大对象初始化节点

    因为整个构建过程很长,Gradle 3大对象不是一上来就都创建出来了,也是在构建过程中一步步才 new 出来的,在此之前我们就使用该对象是会报错的,所以我们必须明确 Gradle 3大对象 从哪个 hook 函数开始可以使用

    换种说法 --> 我们虽然写的是脚本,但是实际代码还是要动态编译成 java 对象执行的,我们必须考虑在脚本中使用对象的时候,对象是不是已经初始化好了

    1. Gradle 对象:

    init.gradle 脚本执行过后,Gradle 就被创建出来了,所以我们在 settings.gradle 脚本里可以尽情使用 Gradle 对象。这也是 settings.gradle 脚本成为我们进行全局 hook 设置的原因

    2. Setting 对象:

    gradle.settingsEvaluated {
        .... settings 对象可以使用了
    

    settings.gradle 脚本执行过后,Setting 对象才算是可以让我们随便使用

    3. Project 对象:

    gradle.projectsLoaded {
        .... project 对象可以使用了
    

    settings.gradle 脚本执行过程中会把所有子项目的 Project 对象创建出来,projectsLoaded() 这个函数中我们就能拿到所有所有 project 对象了

    其实在每个脚本中,都可以使用该脚本对应的核心模型对象,脚本都跑起来了,对象就肯定已经创建出来了。我们要注意的是在脚本中使用超出本脚本范围的对象

    Settings.gradle 脚本可以使用的 Hook 函数

    方法注释和日志结合起来一起看

    include ':libs'
    include ':app'
    buildscript {
    println("settings...")
    // Setting 脚本执行前调用
    gradle.beforeSettings {
        // 在这里写明显无用
        println("gradle.beforeSettings...")
    // Setting 项目编译前调用
    gradle.beforeProject {
        // 在这里写明显无用
        println("gradle.beforeProject...")
    // Setting 脚本执行完成后调用
    gradle.settingsEvaluated {
        println("gradle.settingsEvaluated...")
    // Setting 项目编译完成后调用
    gradle.afterProject {
        println("gradle.afterProject...")
    // 所有项目脚本执行完后调用
    gradle.projectsEvaluated {
        println("gradle.projectsEvaluated...")
    // 开始进入编译阶段时调用
    gradle.projectsLoaded {
        println("gradle.projectsLoaded...")
    // 构建结束时调用
    gradle.buildFinished {
        println("gradle.buildFinished...")
    // 对所有项目脚本进行设置
    gradle.allprojects(new Action<Project>() {
        @Override
        void execute(Project project) {
            // 在这里设置 beforeEvaluate 就能能作用了
            project.beforeEvaluate {
                println("gradle.allprojects.beforeEvaluate...")
            project.afterEvaluate {
                println("gradle.allprojects.afterEvaluate...")
    // 编译阶段 Task 流程图计算出来后调用
    gradle.taskGraph.whenReady {
        println("gradle.taskGraph.whenReady...")
    

    运行日志:

    settings...
    gradle.settingsEvaluated...
    gradle.projectsLoaded...
    > Configure project : --> 执行根目录脚本
    gradle.beforeProject...
    gradle.allprojects.beforeEvaluate...
    root build.gradle...
    gradle.afterProject...
    gradle.allprojects.afterEvaluate...
    root_project.afterEvaluate...
    > Configure project :app --> 执行 app 壳工程脚本
    gradle.beforeProject...
    gradle.allprojects.beforeEvaluate...
    app build.gradle...
    gradle.afterProject...
    gradle.allprojects.afterEvaluate...
    > Configure project :libs --> 执行 libs 子项目脚本
    gradle.beforeProject...
    gradle.allprojects.beforeEvaluate...
    libs build.gradle...
    gradle.afterProject...
    gradle.allprojects.afterEvaluate...
    gradle.projectsEvaluated...
    gradle.taskGraph.whenReady...
    > Task :prepareKotlinBuildScriptModel UP-TO-DATE  --> 执行所有 Task 任务,这里省略了
    gradle.buildFinished...
    BUILD SUCCESSFUL in 2s
    

    子项目 build.gradle 脚本可以使用的 Hook 函数

    Settings.gradle 脚本能写的,这里都可以写。但是写在这里又有什么用呢,都开始跑具体的子项目构建脚本了,你再给全局添加 hook 不就晚了,就算设置了,也只能在该脚本之后脚本才能生效

    能写的 hook 其实就是这2个了,日志输出看上面的就行

    project.beforeEvaluate {
        // 写这里没用
        println("project.beforeEvaluate...")
    project.afterEvaluate {
        println("root_project.afterEvaluate...")
    

    Listener 类型的 hook 函数

    Gradle Listener 监听也是推荐写在 Settings.gradle 脚本里,这个位置比较合适,root build.gradle 勉勉强强可以写,有几个 hook 函数会不起作用,因为其 hook 点都已经过去了

    Gradle 添加 Listener 的方式很灵活,addListener() 函数接收的参数是 Object 对象,可以支持很多种类型的监听,详细看代码提示

    1)addBuildListener()

    函数可以代替一些全局设置的 hook 函数,里面的方法都是上面一些 hook 函数的重复

    gradle.addBuildListener(new BuildListener() {
        @Override
        void buildStarted(Gradle gradle) {
            println("gradle.addBuildListener.buildStarted...")
        @Override
        void settingsEvaluated(Settings settings) {
            println("gradle.addBuildListener.settingsEvaluated...")
        @Override
        void projectsLoaded(Gradle gradle) {
            println("gradle.addBuildListener.projectsLoaded...")
        @Override
        void projectsEvaluated(Gradle gradle) {
            println("gradle.addBuildListener.projectsEvaluated...")
        @Override
        void buildFinished(BuildResult result) {
            println("gradle.addBuildListener.buildFinished...")
    

    2) TaskExecutionGraphListener

    可以监控所有 task 函数的执行

    gradle.addListener(new TaskExecutionGraphListener() {
        @Override
        void graphPopulated(TaskExecutionGraph graph) {
            println("gradle.addListener.hasTask..."+graph.hasTask(":test"))
            graph.getAllTasks().each { 
                it.doLast {
                    // ...
                it.doFirst {
                    // ...
    

    3) TaskExecutionListener

    在 Task 执行前后添加钩子

    gradle.addListener(object : TaskExecutionListener {
        override fun beforeExecute(task: Task) {
        override fun afterExecute(task: Task, state: TaskState) {
    

    4) TaskActionListener

    在 action 执行前后添加钩子

    gradle.addListener(object : TaskActionListener {
        override fun beforeActions(task: Task) {
        override fun afterActions(task: Task) {
    

    5) addTaskExecutionGraphListener

    同 whenReady 的效果

    gradle.getTaskGraph().addTaskExecutionGraphListener(new TaskExecutionGraphListener() {
        @Override
        void graphPopulated(TaskExecutionGraph graph) {
    

    6) DependencyResolutionListener

    Gradle 会在所有子项目脚本执行后,进行依赖的决议。beforeResolve / afterResolve 方法就是 Gradle 为这项工作单独设置的一对 hook

    gradle.addListener(object : DependencyResolutionListener {
        override fun beforeResolve(dependencies: ResolvableDependencies) {
        override fun afterResolve(dependencies: ResolvableDependencies) {
    

    部分函数使用注意点

    1. 判断是否包含指定 Task 要加:号

    这个挺坑人的,一开始我都想到这里,发现总也找不到指定 Task,还有自定义 Task 要不在构建过程中不调用执行的话,Task 执行流程图里是找不到这个 Task 的

    task test {
        println("task--test...")
        doLast {
            println("task--test.doLast...")
    gradle.addListener(new TaskExecutionGraphListener() {
        @Override
        void graphPopulated(TaskExecutionGraph graph) {
            println("gradle.addListener.hasTask..."+graph.hasTask(":test"))
            graph.getAllTasks().each { 
                it.doLast {
                    // ...
                it.doFirst {
                    // ...
    

    所有 hook 流程图

    图来自掘金小册,大体都在图里面,大家最后结合这张图再理解下

    Hook 函数的意义

    是给大家自定义 Task、Plugin 准备的,自定义的任务、插件中的任务你总得给他设置一个执行时机不是,所以 Hook 勾子就成了一个好的选择

    Project API

    Gradle 脚本中我们使用最多的,最让人头疼的就是 Project 对象的 API 了,感谢:深度探索 Gradle 自动化构建技术(三、Gradle 核心解密),我在这里整理一下

    1. getAllprojects()

    这个和 gradle.allProjects() 是一样的

    // index = 0 是 Root buils.gradle 
    project.getAllprojects().eachWithIndex { Project project, int index ->
        if (index == 0) println("index 0 :root project") else println("index $index : $project.name")
    

    2. getSubprojects()

    获取所有子项目

    project.getSubprojects().eachWithIndex { Project project, int index ->
        println("index $index : $project.name")
    

    3. getParent()

    根项目对象获取到的是 null

    project.getParent()
    

    4. getRootProject()

    project.getRootProject()
    

    5. project()

    通过 name 获取到指定子项目,当然 Gradle 对象也有 find 方法,但是这个写法我是第一次看见,有点意思

    project("app") { Project project ->
        apply plugin: 'com.android.application'
    

    6. allprojects() / subprojects

    这个不用说了,大家都熟悉,subprojects 是不会操作跟项目的

    7. plugins

    if (project.plugins.hasPlugin("com.android.library")) {
    	apply from: '../publishToMaven.gradle'
    

    ext 扩展属性

    ext{...} 这个 DSL 代码段也是 Project 对象提供的方法。ext{...} 大家都不陌生吧,都是用来做全局参数、依赖的配置。ext{...} 是 Gradle 提供的、让我们定义所需全局变量的代码块,简称:扩展属性

    1. 定义 ext

    一般我们在根项目脚本中写 ext{...}

    // root build.gradle
    ext {
        tag = "BB"
        age = 2
    

    2. 使用 ext

    大家注意这2种使用方式自动提示的环节,很多人抱怨没有代码提示好难用

    // app build.gradle
    // 方式1:直接使用,sync 之后出现自动提示
    println( "name = $tag" )
    println( "age = $age" )
    // 方式1:借助 rootProject 对象,rebuild 之后出现自动提示
    println( "name = ${rootProject.ext.tag}" )
    println( "age = ${rootProject.ext.age}" )
    

    3. 进阶使用 1 -- 抽象公共配置脚本

    这个好理解,ext{...} 写在根目录脚本里有的人说应该拆分、专人专事,于是我们专门写一个 ext{...} 的脚本出来,该脚本一般都叫:config.gradle,然后 apple from 导入本目录脚本就能用了

    ext{...} 编译过后是 Project 对象中的一个成员属性,ext{...} 中一般我们都是写 Map 来配置一些属性

    // config.gradle
    ext {
    	// 不同的 DSL 配置块,推荐专门写一个 map 出来,这样方便查找
        android = [
                compileSdkVersion: 29,
                applicationId    : "com.bloodcrown.myapplication22",
                minSdkVersion    : 21,
                targetSdkVersion : 29,
                versionCode      : 1,
                versionName      : "1.0"
    
    // root builf.gradle
    apply from: this.file("config.gradle")
    buildscript {
        ext.kotlin_version = "1.4.10"
        repositories {
            google()
            jcenter()
        dependencies {
            classpath "com.android.tools.build:gradle:4.0.1"
            classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    
    // app build.gradle
    android {
        compileSdkVersion rootProject.ext.android.compileSdkVersion
        defaultConfig {
            applicationId rootProject.ext.android.applicationId
            minSdkVersion rootProject.ext.android.minSdkVersion
            targetSdkVersion rootProject.ext.android.targetSdkVersion
            versionCode rootProject.ext.android.versionCode
            versionName rootProject.ext.android.versionName
            testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    

    4. 进阶使用 2 -- 抽象子项目脚本基类

    这个也不难理解,我们的项目要是有多个 子项目 ,每个子项目中 重复的脚本配置 写起来也是让人讨厌的事,尤其是休休改改的情况下很讨厌的,我们应该延续 java 相面对象中的思路:一处修改,处处使用

    这里我们需创建脚本基类,该脚本一般都叫:base_build.gradle,也是通过 apple from 导入子项目脚本就可以了

    // base_build.gradle
    apply plugin: 'com.android.application'
    apply plugin: 'kotlin-android'
    apply plugin: 'kotlin-android-extensions'
    android {
        compileSdkVersion rootProject.ext.android.compileSdkVersion
        defaultConfig {
            applicationId rootProject.ext.android.applicationId
            minSdkVersion rootProject.ext.android.minSdkVersion
            targetSdkVersion rootProject.ext.android.targetSdkVersion
            versionCode rootProject.ext.android.versionCode
            versionName rootProject.ext.android.versionName
            testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        kotlinOptions {
            jvmTarget = '1.8'
    dependencies {
        implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
        implementation 'androidx.core:core-ktx:1.3.2'
        implementation 'androidx.appcompat:appcompat:1.2.0'
        implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
        testImplementation 'junit:junit:4.12'
        androidTestImplementation 'androidx.test.ext:junit:1.1.2'
        androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
    
    // app build.gradle
    apply from: rootProject.file("base_build.gradle")
    dependencies {
        implementation fileTree(dir: "libs", include: ["*.jar"])
        implementation 'com.google.android.material:material:1.0.0'
        implementation 'androidx.navigation:navigation-fragment-ktx:2.1.0'
        implementation 'androidx.navigation:navigation-ui-ktx:2.1.0'
    

    5. 进阶使用 3 -- 使用遍历代替一行行手写

    可能使有的项目依赖实在是太多了吧,于是出现了一种在 ext{...} 中写依赖,然后在脚本中遍历添加依赖的方式,方便是放便了,但是看着有些不习惯

    dependencies = [...] annotationd_ependencies = [...] dependencies.each { k, v -> implementation v } annotationd_ependencies.each { k, v -> implementation v }

    嘛~ 这种思路不是太能接受,大家看需求吧

    6. ext{...} 中一样可以写代码

    出了写一些配置参数,我们一些可以写代码的,看个例子:

    ext {
      versionName = rootProject.ext.android.versionName
      versionCode = rootProject.ext.android.versionCode
      versionInfo = 'App的第2个版本,上线了一些最基础核心的功能.'
      destFile = file('releases.xml')
      if (destFile != null && !destFile.exists()) {
        destFile.createNewFile()
    this.project.afterEvaluate { project ->
      def buildTask = project.tasks.findByName("build")
      doLast {
        buildTask.doLast {
          writeTask.execute()
    task writeTask {......}
    
    (project.tasks.findByName(chmodTask.name) as Task).dependsOn(mergeResourcesTask.taskDependencies.getDependencies(mergeResourcesTask))
    (project.tasks.findByName(mcPicTask.name) as Task).dependsOn(project.tasks.findByName(chmodTask.name) as Task)
    mergeResourcesTask.dependsOn(project.tasks.findByName(mcPicTask.name))
    复制代码
    分类:
    Android
    标签: