Android—Gradle教程(二)

4,922 阅读10分钟

前言

在上一篇文章中,对Gradle基础以及构建机制进行了详细的讲解,在这一篇中将会对Gradle核心模型以及Gradle插件进行讲解。

1.Gradle核心模型

1.1 Gradle钩子函数

讲钩子函数,还是得拿出Gradle执行流程图

图片1.png

如图所示

  • gradle在生命周期三个阶段都设置了相应的钩子函数调用。
  • 使用钩子函数,处理自定义的构建:
    • 初始化阶段:gradle.settingsEvaluated和gradle.projectsLoaded。(在settings.gradle中生效)
    • 配置阶段:project.beforeEvaluate和project.afterEvaluate;gradle.beforeProject、gradle.afterProject及gradle.taskGraph.taskGraph.whenReady。
    • 执行阶段:gradle.taskGraph.beforeTask和gradle.taskGraph.afterTask。

而Gradle也可以监听各个阶段的回调处理:

  • gradle.addProjectEvaluationListener
  • gradle.addBuildListener
  • gradle.addListener:TaskExecutionGraphListener (任务执行图监听),TaskExecutionListener(任务执行监听),TaskExecutionListener、TaskActionListener、StandardOutputListener ...

概念又说了一大堆,撸码验证一下!

  1. 打开AS,创建一个普通工程项目。
  2. 进入项目build.gradle(外层)文件
  3. 在末尾添加如下代码:
// =======================================
// Gradle提供的钩子函数
// 配置阶段:
gradle.beforeProject {
    println "gradle.beforeProject"
}
gradle.afterProject {
    println "gradle.afterProject"
}
gradle.taskGraph.whenReady {
    println "gradle.taskGraph.whenReady"
}
beforeEvaluate {

    println "beforeEvaluate"
}
afterEvaluate {
    println "afterEvaluate"
}


//==================
// 为gradle设置监听
gradle.addProjectEvaluationListener(new ProjectEvaluationListener() {
    @Override
    void beforeEvaluate(Project project) {
        println "Configure listener beforeEvaluate"
    }

    @Override
    void afterEvaluate(Project project, ProjectState state) {
        println "Configure listener afterEvaluate"
    }
})


gradle.addBuildListener(new BuildListener() {
    @Override
    void buildStarted(Gradle gradle) {
        println "Build listener buildStarted"
    }

    @Override
    void settingsEvaluated(Settings settings) {
        println "Build listener settingsEvaluated"
    }

    @Override
    void projectsLoaded(Gradle gradle) {
        println "Build listener projectsLoaded"
    }

    @Override
    void projectsEvaluated(Gradle gradle) {
        println "Build listener projectsEvaluated"
    }

    @Override
    void buildFinished(BuildResult result) {
        println "Build listener buildFinished"
    }
})

task runGradle{
    println "configure runGradle AAAAAA"
    doFirst {
        println "doFirst runGradle AAAAAA"
    }
}

代码解析

最上面那段代码就是上一篇文章也写过相同的,随后为Gradle设置了配置监听以及运行监听。然后我们运行一下这个runGradle任务看下效果:

Starting Gradle Daemon...
Connected to the target VM, address: '127.0.0.1:65159', transport: 'socket'
Gradle Daemon started in 2 s 697 ms

> Configure project :
configure runGradle AAAAAA
Configure listener afterEvaluate
gradle.afterProject
afterEvaluate

> Configure project :app
Configure listener beforeEvaluate
gradle.beforeProject
Configure listener afterEvaluate
gradle.afterProject
Build listener projectsEvaluated
gradle.taskGraph.whenReady

> Task :runGradle
doFirst runGradle AAAAAA
Build listener buildFinished

BUILD SUCCESSFUL in 8s
1 actionable task: 1 executed
14:46:28: Task execution finished 'runGradle'.
Disconnected from the target VM, address: '127.0.0.1:65159', transport: 'socket'

从这个运行效果可以看出,配置阶段它暂时分为了两个(因为现在只有Project以及app的Gradle),在配置project.gradle的时候,并没有执行beforeEvaluatebeforeProject这两个方法;而这两个方法却在配置app.gradle的时候执行了。

所以上一篇留下的小瑕疵在这里得到了最终解释(为什么配置阶段没运行那两方法),因为在配置project.gradle的时候,是不会运行那两方法的。

现在继续回到运行效果这里,这次重点放在前三句以及末尾几句。

我们在使用AndroidStudio编译项目的时候,往往都是第一次编译的很慢,但只要编译好了,当天再次编译的时候就非常快;而编译好的项目长时间不编译也会出现编译很慢的情况,这是什么原因呢?

答案就在于:Starting Gradle Daemon... 这段代码。

1.2 Gradle守护进程(Daemon)

项目启动时,会开启一个client,然后启动一个Daemon,通过client向daemon收发请求,项目关闭,client关闭,Daemon保持启动,有类似项目再次部署时,会直接通过新的client访问已经启动的Daemon,所以速度很快,默认daemon不使用3小时后关闭;不同项目兼容性考虑,也可使用--no-daemon 启动项目,就没有速度优势了。

所以在这个运行效果里面能看到: Connected to the target VM, address 运行开始,连接Daemon Disconnected from the target VM, address 运行结束,关闭连接Daemon

在我们使用Gradle的时候,当有多个library工程项目时,往往会对版本进行统一化,因此这就需要使用Gradle属性的扩展功能。

1.3 Gradle属性扩展

  • 使用ext对任意对象属性进行扩展:

    • 对project进行使用ext进行属性扩展,对所有子project可见。
    • 一般在root project中进行ext属性扩展,为子工程提供复用属性,通过rootProject直接访问
    • 任意对象都可以使用ext来添加属性:使用闭包,在闭包中定义扩展属性。直接使用=赋值,添加扩展属性。
    • 由谁进行ext调用,就属于谁的扩展属性。
    • 在build.gradle中,默认是当前工程的project对象,所以在build.gradle直接使用"ext="或者"ext{}"其实就是给project定义扩展属性
  • 使用gradle.properties以键值对形式定义属性,所有project可直接使用

1.3.1 使用ext对任意对象属性进行扩展

在project.gradle里添加如下代码

ext {// project 属性扩展,能在别的工程可见
    prop1 = "prop1"
    prop3 = "prop3"
}

ext.prop2 = "prop2"

println prop1
println prop2

task runProExtPro{
    println "runProExtPro\t"+project.ext.prop3
    println "runProExtPro\t"+project.prop2
}

运行任务runProExtPro后的效果

...略
prop1
prop2
runProExtPro	prop3
runProExtPro	prop2
...略

从这个运行效果可知通过ext这个属性会开启一个闭包,在闭包内可以进行多属性扩展,扩展后,也可在外部进行单属性扩展。因为这里访问是在当前project.gradle环境下运行的,现在在app.gradle里面访问试试。

task runAppExtPro{
    println "runAppExtPro\t"+project.prop3
    println "runAppExtPro\t"+project.prop2
}

注意看,这里已经把ext给去掉了,因为在这加上会提示对应属性不存在,所以在访问ext扩展属性时,推荐直接通过project.xx的方式直接访问。现在来看看运行runAppExtPro效果:

...略
runAppExtPro	prop3
runAppExtPro	prop2
...略

从这里可以看出:对project进行使用ext进行属性扩展,对所有子project可见。

当我们配置版本信息的时候,不想吧扩展属性,配置在根project.gradle里面的时该怎么办呢?此时就有了另一种扩展方式。

1.3.2 使用gradle.properties定义属性

打开gradle.properties,在里面添加如下属性:

MIN_SDK_VERSION=21
TARGET_SDK_VERSION=30
COMPILE_SDK_VERSION=30
BUILD_TOOL_VERSION=30.0.3

打开对应子project.gradle或者我们依赖的library库,就可以使用我们刚刚扩展的属性。

android {
    compileSdkVersion Integer.parseInt(COMPILE_SDK_VERSION)
    buildToolsVersion BUILD_TOOL_VERSION

    defaultConfig {
        applicationId "com.hqk.gradledemo01"
        minSdkVersion Integer.parseInt(MIN_SDK_VERSION)
        targetSdkVersion Integer.parseInt(TARGET_SDK_VERSION)
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

   ...略
...略

现在依然能够编译成,并且所有子project都可以统一使用 gradle.properties扩展的属性版本号。当然我们也可以专门写一个task来验证一下: 在app.gradle里,添加如下代码

task checkVersion{
    println "runAppGradle"
    println "MIN_SDK_VERSION:"+MIN_SDK_VERSION
    println "TARGET_SDK_VERSION:"+TARGET_SDK_VERSION
    println "COMPILE_SDK_VERSION:"+COMPILE_SDK_VERSION
    println "BUILD_TOOL_VERSION:"+BUILD_TOOL_VERSION
}

运行效果

...略
runAppGradle
MIN_SDK_VERSION:21
TARGET_SDK_VERSION:30
COMPILE_SDK_VERSION:30
BUILD_TOOL_VERSION:30.0.3
...略

完美运行,也打出来想要的效果。不过到这为止,写的task几乎都是打印输出,都还没写过复杂逻辑。那么如果想要实现复杂逻辑,要怎样定义task呢?

1.4 Gradle自定义任务

在build.gradle中自定义任务:

  • task定义的任务其实就是DefaultTask的一种具体实现类的对象
  • 可以使用自定义类继承DeaflutTask:
    • 在方法上使用@TaskAction注解,表示任务运行时调用的方法。
    • 使用@Input表示对任务的输入参数。
    • 使用@OutputFile表示任务输出文件。
    • 使用inputs,outputs直接设置任务输入/输出项。
    • 一个任务的输出项可以作为另一个任务的输入项 (隐式依赖关系)。

1.4.1 文件数据写入Demo

class WriteTask extends DefaultTask {
    @Input
//    @Optional
    // 表示可选
    String from
    
    @OutputFile
//    @Optional
    // 表示可选
    File out
    
    WriteTask() {

    }
    @TaskAction
    void fun() {
        println " @TaskAction fun()"
        println from
        println out.toString()
        out.createNewFile()
        out.text=from
    }
}

task myTask(type: WriteTask) {
    from = "a/b/c" // 输入
    out = file("test.txt") // 输出
}

从这段代码可知,定义了一个WriteTask自定义任务,里面两个属性,分别用对应注解表示输入输出对象,随后定义了myTask 任务,将字符串写入file文件里,运行来看看效果。

QQ截图20211028172206.png

如图所示

当Gradle运行成功时,同级目录下新增了txt文件,里面的内容就是我们刚刚写入字符串。现在这个demo来升级一下,目前是一个字符串写入文件,那么能不能将一个文件的内容写入在另一个文件里呢?现在来试试:

class WriteTask extends DefaultTask {
////    @Input
////    @Optional
//    // 表示可选
//    String from
////    @OutputFile
////    @Optional
//    // 表示可选
//    File out
    WriteTask() {

    }
    @TaskAction
    void fun() {
        println " @TaskAction fun()"
//        println from
//        println out.toString()
//        out.createNewFile()
//        out.text=from
        println inputs.files.singleFile
        def inFile = inputs.files.singleFile

        def file = outputs.files.singleFile
        file.createNewFile()
        file.text = inFile.text

    }
}

task myTask(type: WriteTask) {
//    from = "a/b/c" // 输入
//    out = file("test.txt") // 输出
    inputs.file file('build.gradle')
    outputs.file file('test.txt')
}

现在将输入输出的方式改了,通过inputs.outputs.的方式进行输入输出。里面逻辑是将build.gradle里面的内容写入test.txt里面,运行看看效果:

QQ截图20211028173645.png

从这里看出,已经成功将build.gradle里面的内容写入test.txt里面了。

到这里,数据写入demo 已经写完了。现在开始新的demo:文件压缩

1.4.2 文件压缩Demo

app.gradle里面添加如下代码:

task zip(type: Zip) {
    archiveName "outputs.zip"// 输出的文件名字
    destinationDir file("${buildDir}/custom")// 输出的文件存放的文件夹
    from "${buildDir}/outputs"// 输入的文件
}

通过这段代码可知,将会吧同级目录下的${buildDir}运行成功的build目录下的outputs文件里面的内容进行压缩处理。

注意:这里之所以会压缩,注意型参,类型为Zip,表示启用的是Zip压缩任务。就和我们刚刚自定义的文件写入形参类型为type: WriteTask

现在运行task zip看看效果:

QQ截图20211028180204.png

从这个效果图可知:这个压缩已经成功压缩了。但问题来了,因为单独执行task zip任务是不会启用APK编译的,因为两者并没有任何关联(上一篇讲解过),那么如果压缩的目标不存在(apk并没有编译生成对应的build文件夹)会怎样?吧目标文件夹删除试一下:

运行效果

...略

> Task :app:zip NO-SOURCE
Build listener buildFinished

注意看,这里提示 NO-SOURCE,并没有任何资源,也就是压缩失败了。那么能不能等压缩目标创建 好了再来压缩呢?或者说,执行压缩任务的时候,就算目标任务不存在也要提前编译好后再来压缩。

现在继续改造代码:

//task zip(type: Zip) {
//    archiveName "outputs.zip"// 输出的文件名字
//    destinationDir file("${buildDir}/custom")// 输出的文件存放的文件夹
//    from "${buildDir}/outputs"// 输入的文件
//}

afterEvaluate {
    println tasks.getByName("packageDebug")
    task zip(type: Zip) {
        archiveName "outputs2.zip"// 输出的文件名字
        destinationDir file("${buildDir}/custom")// 输出的文件存放的文件夹
        from tasks.getByName("packageDebug").outputs.files// 输入的文件
        tasks.getByName("packageDebug").outputs.files.each {
            println it
        }
    }
}

在这里我将压缩任务转移到了app.gradleafterEvaluate 闭包里面,也就是说,apk在编译配置即将结束的时候,会将task zip任务,注入在Gradle执行流程里,当单独运行task zip任务的时候,因为它在apk编译执行流程里面,所以它就会启动apk的编译,随后执行task zip任务就能达到想要的效果了。现在继续单独运行task zip任务试试:

注意:形参type: Zip的任务只能存在一个,所以要把外面的注释掉

QQ截图20211028181753.png

代码这没有运行按钮了,那么就用右边工具来辅助运行,注意左边并没有编译好的文件夹,点击右边运行:

QQ截图20211028181939.png

运行结束后,左边如愿以偿多了对应的build文件夹,里面也有对应的压缩包,而且名字也能对上。

到这里文件压缩demo已经完美的实现了,但是这个功能只能给你自己这一个项目使用,那万一想给他人使用或者说给其他项目使用怎么办呢?那这个就遇到用到插件了。

2.Gradle插件

2.1 什么是Gradle插件

  • Gradle插件是提供给gradle构建工具,在编译时使用的依赖项。插件的本质就是对公用的构建业务进行打包,以提供复用
  • Gradle插件分为:脚本插件和二进制插件 (实现Plugin的类)
  • Gradle插件通过apply方法引入到工程

这里说到Gradle插件分为:脚本插件和二进制插件,那么对应有何区别?

  • 脚本插件实现了一些列的任务,并且进行了组装,按照提供的API就可以直接使用
  • Gradle脚本插件,是提供实现的任务封装,需要自行组装。或者是用到的一些具体业务的封装。

2.2 Gradle 脚本插件

既然是脚本,那么就创建对应的脚本:在项目根目录创建脚本文件script.gradle,里面写入代码:

afterEvaluate {
    println tasks.getByName("packageDebug")
    task zip(type: Zip) {
        archiveName "outputs3.zip"// 输出的文件名字
        destinationDir file("${buildDir}/custom")// 输出的文件存放的文件夹
        from tasks.getByName("packageDebug").outputs.files// 输入的文件
        tasks.getByName("packageDebug").outputs.files.each {
            println it
        }
    }
}

仔细 看这个脚本,可以发现:脚本插件里面的内容和刚刚我们在app.gradle里面写入的内容一模一样,接下来按照apply方法引入到工程试试:

进入app.gradle里面


apply from: '../script.gradle'

android {
    compileSdkVersion Integer.parseInt(COMPILE_SDK_VERSION)
    buildToolsVersion BUILD_TOOL_VERSION
    
...略
}

//task zip(type: Zip) {
//    archiveName "outputs.zip"// 输出的文件名字
//    destinationDir file("${buildDir}/custom")// 输出的文件存放的文件夹
//    from "${buildDir}/outputs"// 输入的文件
//}

//afterEvaluate {
//    println tasks.getByName("packageDebug")
//    task zip(type: Zip) {
//        archiveName "outputs2.zip"// 输出的文件名字
//        destinationDir file("${buildDir}/custom")// 输出的文件存放的文件夹
//        from tasks.getByName("packageDebug").outputs.files// 输入的文件
//        tasks.getByName("packageDebug").outputs.files.each {
//            println it
//        }
//    }
//}

记得这里要把刚刚的压缩注释掉。现在继续点击右边的运行看看效果:

QQ截图20211028183456.png

从这个效果上看,已经完美运行成功!脚本插件就这么简单!那么二进制插件又该是怎样的?

2.3 Gradle 二进制插件

//apply from: '../script.gradle'
apply plugin: MyPlugin

android {
    compileSdkVersion Integer.parseInt(COMPILE_SDK_VERSION)
    buildToolsVersion BUILD_TOOL_VERSION
...略
}

//task zip(type: Zip) {
//    archiveName "outputs.zip"// 输出的文件名字
//    destinationDir file("${buildDir}/custom")// 输出的文件存放的文件夹
//    from "${buildDir}/outputs"// 输入的文件
//}
//afterEvaluate {
//    println tasks.getByName("packageDebug")
//    task zip(type: Zip) {
//        archiveName "outputs2.zip"// 输出的文件名字
//        destinationDir file("${buildDir}/custom")// 输出的文件存放的文件夹
//        from tasks.getByName("packageDebug").outputs.files// 输入的文件
//        tasks.getByName("packageDebug").outputs.files.each {
//            println it
//        }
//    }
//}


//=============================================
// 插件:1. 脚本插件
// 2. 二进制插件

class MyPlugin implements Plugin<Project> {

    @Override
    void apply(Project target) {
        println "MyPlugin apply"

        target.afterEvaluate {
            println "MyPlugin afterEvaluate "+target.tasks.getByName("packageDebug")
            target.task(type: Zip, "zip") {//第二个参数要指定是哪个方法
                archiveName "outputs4.zip"// 输出的文件名字
                destinationDir target.file("${target.buildDir}/custom")// 输出的文件存放的文件夹
                from target.tasks.getByName("packageDebug").outputs.files// 输入的文件
                target.tasks.getByName("packageDebug").outputs.files.each {
                    println it
                }
            }
        }
    }

这里看到,定义了MyPlugin 类实现了对应的Plugin<Project> 接口,在对应的target.afterEvaluate里面定义了任务target.task(type: Zip, "zip"),第一个参数明确什么类型,第二个参数表示当前任务名为zip压缩。

现在删除之前运行的结果,继续运行右边的任务,看看效果:

QQ截图20211028184718.png

哈哈哈,这个插件也如期的运行成功了。到这里这篇教程差不多就结束了。

3. 结束语

相信看到这里的小伙伴,对Gradle的核心模型以及Gradle插件有了一个全新的认知。在下一篇里,将会继续深入Gradle讲解。

原创不易,如果本篇文章对小伙伴们有用,希望小伙伴们多多点赞支持一下。笔者也好更快更好的更新教程。