Gradle 核心之 Task

一、前言


只有 Task 才可以在 Gradle 的执行阶段去执行(其实质是执行的 Task 中的一系列 Action),所以 Task 的重要性不言而喻。

二、Task


2.1 Task 定义与配置

Task 的定义方式有如下两种:

Task 的配置方式也有如下两种:

配置了 group 后可以在 Android Studio 的 Gradle 面板看到对应的 Task Group 及其分组下的 Tasks,如下图所示:

一般来说都推荐为我们的 task 配置 group,便于我们查找 task。另外,group 和 descreption 只是最基本的配置,我们看下 Task 的源码:

可以看到这些属性都是可以进行配置的,后面会一一讲解。

选型描述默认值
nametask 名字无,必须指定
type需要创建的 task ClassDefaultTask
action当 task 执行的时候,需要执行的闭包 closure 或 行为 Actionnull
overwrite替换一个已存在的 taskfalse
dependsOn该 task 所依赖的 task 集合[]
group该 task 所属组null
descriptiontask 的描述信息null
constructorArgs传递到 task Class 构造器中的参数null

2.2 Task 执行

上一节我们定义了两个 task,我们来执行其中一个:

看到上述输出我们会有个疑问,为什么我们执行 helloTask2,但是 helloTask 也被执行输出了呢?其实很简单,因为这两个 task 都是在 gradle 配置阶段执行的,所以我们任何 task 的执行,我们 project 的整个配置代码都是会执行的,所以这两个输出语句都会被执行到。

我们可以通过添加 doFirst 与 doLast 执行动作(Action)为我们的 task 指定执行阶段要执行的代码,这样它就只会在 gradle 执行阶段去执行。需要注意的是,doFirst 和 doLast 是可以被执行多次的。对于 doFirst 与 doLast 这两个 Action,它们的作用分别如下所示:

  • doFirst:表示 task 执行最开始的时候被调用的 Action。
  • doLast:表示 task 将执行完的时候被调用的 Action。

我们来验证下:

接下来,我们就使用 doFirst 与 doLast 来进行一下实战,来实现计算 build 执行期间的耗时,其完整代码如下所示:

2.3 Task 执行顺序

指定 Task 的执行顺序有三种方式,如下图所示:

2.3.1 dependsOn 强依赖方式

dependsOn 强依赖的方式可以细分为静态依赖和动态依赖,首先看看静态依赖,如下所示:

taskZ 依赖 taskX 和 taskY:

执行 taskZ 看看:

可以看到被依赖的 task 先执行,这和我们 java 的继承关系是很相似的。需要注意的是,这里 taskX 和 taskY 的执行顺序是随机的。

下面我们再来看看动态依赖:

2.3.2 通过Task输入输出指定

我们也可以通过 Task 来指定输入输出,Task 的输入输出对应 TaskInput 和 TaskOutput。下面我们来看一个示例,使用这种方式实现一个自动维护版本发布文档的 gradle 脚本,其中输入输出相关的代码如下所示:

首先,我们定义了一个 WirteTask,然后,在注释1处,指定了输出文件为 destFile, 并写入版本信息到 XML 文件。接着,定义了一个 readTask,并在注释2处,指定输入文件为上一个 task(即 writeTask) 的输出文件。最后,在注释3处,使用 dependsOn 将这两个 task 关联起来,此时输入与输出的顺序是会先执行写入,再执行读取。这样,一个输入输出的实际案例就实现了。

2.3.3 通过API指定执行顺序

除了 dependsOn 的方式,我们还可以在 task 闭包中通过 mustRunAfter 方法指定 task 的依赖顺序,mustRunAfter 可以指定一个或多个 task,其示例代码如下所示:

下面我们在命令行中将 taskX、taskY、taskZ 打乱执行:

可以看到最终的执行顺序始终是 taskX、taskY、taskZ。

mustRunAfter 是强制指定顺序,另外还有一个 shouldRunAfter 不强制性指定,实际应用中一般不会使用 shouldRunAfter,了解一下即可。

2.4 挂接自定义 task 到构建生命周期

我们可以使用 gradle 提供的一系列生命周期 API 去挂接我们自己的 task 到构建生命周期之中,比如使用 afterEvaluate 方法将我们第三小节定义的 writeTask 挂接到 gradle 配置完所有的 task 之后的时刻,示例代码如下所示:

2.5 Task 类型

除了定义一个新的 task 之外,我们也可以使用 task 的 type 属性来直接使用一个已有的 task 类型,比如 Gradle 自带的 Copy、Delete、Sync task 等等。示例代码如下所示:

更多的 Task 类型我们可以查阅官方文档。


Android 对 Gradle 的扩展:Variants、Transform

一、前言


本篇我们来学习下 Android 对 Gradle 的扩展:Variants(变体)以及 Transform。通过扩展可以让我们在自定义 Gradle 插件时做更多的事情。

二、Variants(变体)


2.1 Variants 是什么

要理解 Variants 的作用,就必须先了解 buildType、flavor、dimension 与 variant 之间的关系。在 android gradle plugin V3.x 之后,每个 flavor 必须对应一个 dimension,可以理解为 flavor 的分组,然后不同 dimension 里的 flavor 会组合成一个 variant。示例代码如下所示:

    android {
        ...
        defaultConfig {...}
        //gradle 默认就有 debug 和 release 两个 buildType
        buildTypes {
            debug{...}
            release{...}
        }
 
        flavorDimensions "version"
        productFlavors {
            demo {
                dimension "version"
            }
            full {
                dimension "version"
            }
        }
    }

根据上述配置 Gradle 会创建以下构建变体:

  • demoDebug
  • demoRelease
  • fullDebug
  • fullRelease

在 Android 对 Gradle 插件的扩展支持之中,其中最常用的便是利用变体(Variants)来对构建过程中的各个默认的 task 进行 hook。关于 Variants 共有 三种类型,如下所示:

  • applicationVariants:只适用于 app plugin。
  • libraryVariants:只适用于 library plugin。
  • testVariants:在 app plugin 与 libarary plugin 中都适用。

2.2 Variants 的使用

我们来看看 applicationVariants 的使用,首先我们在 app.gradle 中配置 buildTypes、flavorDimensions、productFlavors 同上。然后,我们可以 使用 applicationVariants.all 在配置阶段之后去获取所有 variant 的 name 与 baseName。代码如下所示:

最后我们来执行下 gradle clean 任务:

可以看到,name 与 baseName 的区别:demoDebug 与 demo-debug 。

接下来我们来看看使用 applicationVariants.all 在配置阶段之后去修改输出的 APK 名称:

可以看到,我们上面用到了一个 releaseTime() 方法获取当前时间:

最后我们来执行以下 gradle clean:

可以看到正常修改了 apk 的名称。

最后我们来看一下如何对 applicationVariants 中的 Task 进行 Hook,我们可以在 android.applicationVariants.all 的闭包中通过 variant.task 来获取相应的 Task。代码如下所示:

然后,执行 gradle clean,其输出信息如下所示:既然可以获取到变体中的 Task,我们就可以根据不同的 Task 类型来做特殊处理。例如,我们可以利用 variants 去解决插件化开发中的痛点:编写一个对插件化项目中的各个插件自动更新的脚本,其核心代码如下所示:

至于 update_plugin 的实现,主要就是一些插件安全校验与下载的逻辑,这部分其实跟 Gradle 没有什么联系。

variant 中能获取到哪些 task 我们可以去 ApplicationVariant 的父类 BaseVariant 中去查看,比如:

2.3 Gradle 构建流程

在执行 Android 项目的构建流程,可以发现没有任何修改的情况下就已经有 30 多个Task需要执行:

 其中关键的 task 如下:

三、Transform


Google 官方在 Android Gradle V1.5.0 版本以后提供了 Transfrom API,允许第三方 Plugin 在打包成 .dex 文件之前的编译过程中操作 .class 文件,我们需要做的就是实现 Transform 来对 .class 文件遍历以拿到所有方法,修改完成后再对原文件进行替换即可。总的来说,Gradle Transform 的功能就是把输入的 .class 文件转换为目标字节码文件。我们可以通过 Gradle Plugin 来注册我们编写的 Transform。注册后的 Transform 会被 Gradle 包装成一个 Gradle Task,这个 TransForm Task 会在 java compile Task 执行完毕后运行。

我们来看看 Transform 的执行流程图:

3.1 Transform 的使用

下面我们来看看如何使用 Transform,首先如果是在 buildSrc 中,由于 buildSrc 的执行时机要早于任何一个 project,因此需要添加仓库:

然后,创建一个 Transform 的子类继承自 com.android.build.api.transform.Transform:

可以看到其创建步骤可以细分为五步,如下所示:

3.1.1、getName()

指定自定义 Transform 的名称。返回对应的 Task 名称。

3.1.2、getInputTypes()

可以看到这个方法返回的是一个 Set<QualifiedContent.ContentType> 集合,指明你自定义的这个 Transform 处理的输入类型集合。QualifiedContent.ContentType 是一个接口,它的实现类有 DefaultContentType 和 ExtendedContentType。为了方便 TransformManager 为我们封装了以下几种输入类型集合:

分别代表的是:

  • CONTENT_CLASS:表示需要处理 java 的 class 文件。
  • CONTENT_JARS:表示需要处理 java 的 class 与 资源文件。
  • CONTENT_RESOURCES:表示需要处理 java 的资源文件。
  • CONTENT_NATIVE_LIBS:表示需要处理 native 库的代码。
  • CONTENT_DEX:表示需要处理 DEX 文件。
  • CONTENT_DEX_WITH_RESOURCES:表示需要处理 DEX 与 java 的资源文件。
3.1.3、getScopes()

可以看到这个方法返回的是一个 Set<QualifiedContent.Scope> 集合,用来指明自定义的 Transform 的输入文件所属的范围,这是因为 gradle 是支持多工程编译的。Scope 是一个枚举类:

可以看到目前有 5 种基本类型,分别代表的是:

  • PROJECT:只有项目内容。
  • SUB_PROJECTS:只有子项目。
  • EXTERNAL_LIBRARIES:只有外部库,
  • TESTED_CODE:由当前变体(包括依赖项)所测试的代码。
  • PROVIDED_ONLY:只提供本地或远程依赖项。

同样,为了方便,TransformManager 为我们封装了 getScope 的返回:

如果一个 Transform 不想处理任何输入,只是想查看输入的内容,那么只需在 getScopes() 返回一个空集合,然后在getReferencedScopes() 返回想要接收的范围。

 
  1. public Set<? super Scope> getReferencedScopes() {

  2. return ImmutableSet.of();

  3. }

3.1.4、isIncremental()

isIncremental 方法用于确定是否支持增量更新,如果返回 true,TransformInput 会包含一份修改的文件列表,如果返回 false,则会进行全量编译,并且会删除上一次的输出内容。

3.1.5、transform(TransformInvocation transformInvocation)

它是 Transform 的关键方法,在 transform() 方法中,就是用来给我们进行具体的输入输出转换过程的。它是一个空实现,input 的内容将会打包成一个 TransformInvocation 对象,因为我们要想使用 input,我们需要详细了解一下 TransformInvocation 参数。

public interface TransformInvocation {
 
    // 输入作为 TransformInput 返回
    Collection<TransformInput> getInputs(); 
 
    //TransformOutputProvider 可以用来创建输出内容
    TransformOutputProvider getOutputProvider(); 
 
    boolean isIncremental();
...
}

TransformInput 可认为是所有输入文件的一个抽象,它主要包括两个部分,如下所示:

public interface TransformInput {
    Collection<JarInput> getJarInputs();
    Collection<DirectoryInput> getDirectoryInputs();
}
 
public interface JarInput extends QualifiedContent {
 
    File getFile(); //jar文件
 
    Set<ContentType> getContentTypes(); // 是class还是resource
 
    Set<? super Scope> getScopes();  //属于Scope:
}

其中:

  • DirectoryInput 集合:表示以源码方式参与项目编译的所有目录结构与其目录下的源码文件。
  • JarInput 集合:表示以 jar 包方式参与项目编译的所有本地 jar 包和远程 jar 包。需要注意的是,这个 jar 所指也包括 aar。

TransformOutputProvider 表示 Transform 的输出,利用它我们可以获取输出路径等信息。

public interface TransformOutputProvider {
    //根据 name、ContentType、QualifiedContent.Scope返回对应的文件( jar / directory)
    File getContentLocation(String name, Set<QualifiedContent.ContentType> types, 
        Set<? super QualifiedContent.Scope> scopes, Format format);
}

即我们可以通过 TransformInvocation 来获取输入,同时也获得了输出的功能。举个例子:

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
        println '--------------- MyCustomTransform visit start --------------- '
        def startTime = System.currentTimeMillis()
        def inputs = transformInvocation.inputs
        def outputProvider = transformInvocation.outputProvider
        // 1、删除之前的输出    
        if (outputProvider != null) {
            outputProvider.deleteAll()
        }
 
        // Transform 的 inputs 有两种类型,一种是目录,一种是 jar包,要分开遍历    
        inputs.each { TransformInput input ->
            // 2、遍历 directoryInputs(本地 project 编译成的多个 class⽂件存放的目录)        
            input.directoryInputs.each { DirectoryInput directoryInput ->
                handleDirectory(directoryInput, outputProvider)
            }
            // 3、遍历 jarInputs(各个依赖所编译成的 jar 文件)        
            input.jarInputs.each { JarInput jarInput ->
                handleJar(jarInput, outputProvider)
            }
        }
        def cost = (System.currentTimeMillis() - startTime) / 1000
        println '--------------- MyCustomTransform visit end --------------- ' p
        rintln "MyCustomTransform cost : $cost s"
    }

这里我们主要是做了三步处理,如下所示:

  • 删除之前的输出。
  • 遍历 directoryInputs(本地 project 编译成的多个 class ⽂件存放的目录)。
  • 遍历 jarInputs(各个依赖所编译成的 jar 文件)。

在 handleDirectory 与 handleJar 方法中则是进行了相应的 文件处理 && ASM 字节码修改。

编写完 Transform 的代码之后,我们就可以在 Plugin 的 apply 方法中加入下面代码去注册 TransformTest 的实例,代码如下所示:

  • 29
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值