上一篇学习了如何自定义 Gradle 插件,本篇我们来学习下 Android 对 Gradle 的扩展:Variants(变体)以及 Transform。通过扩展可以让我们在自定义 Gradle 插件时做更多的事情。
要理解 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 会创建以下构建变体:
在 Android 对 Gradle 插件的扩展支持之中,其中最常用的便是利用变体(Variants)来对构建过程中的各个默认的 task 进行 hook。关于 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 中去查看,比如:
在执行 Android 项目的构建流程,可以发现没有任何修改的情况下就已经有 30 多个Task需要执行:
其中关键的 task 如下:
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 的执行流程图:
下面我们来看看如何使用 Transform,首先如果是在 buildSrc 中,由于 buildSrc 的执行时机要早于任何一个 project,因此需要添加仓库:
然后,创建一个 Transform 的子类继承自 com.android.build.api.transform.Transform:
可以看到其创建步骤可以细分为五步,如下所示:
指定自定义 Transform 的名称。返回对应的 Task 名称。
可以看到这个方法返回的是一个 Set
分别代表的是:
可以看到这个方法返回的是一个 Set
可以看到目前有 5 种基本类型,分别代表的是:
同样,为了方便,TransformManager 为我们封装了 getScope 的返回:
如果一个 Transform 不想处理任何输入,只是想查看输入的内容,那么只需在 getScopes() 返回一个空集合,然后在getReferencedScopes() 返回想要接收的范围。
public Set super Scope> getReferencedScopes() {
return ImmutableSet.of();
}
isIncremental 方法用于确定是否支持增量更新,如果返回 true,TransformInput 会包含一份修改的文件列表,如果返回 false,则会进行全量编译,并且会删除上一次的输出内容。
它是 Transform 的关键方法,在 transform() 方法中,就是用来给我们进行具体的输入输出转换过程的。它是一个空实现,input 的内容将会打包成一个 TransformInvocation 对象,因为我们要想使用 input,我们需要详细了解一下 TransformInvocation 参数。
public interface TransformInvocation {
// 输入作为 TransformInput 返回
Collection getInputs();
//TransformOutputProvider 可以用来创建输出内容
TransformOutputProvider getOutputProvider();
boolean isIncremental();
...
}
TransformInput 可认为是所有输入文件的一个抽象,它主要包括两个部分,如下所示:
public interface TransformInput {
Collection getJarInputs();
Collection getDirectoryInputs();
}
public interface JarInput extends QualifiedContent {
File getFile(); //jar文件
Set getContentTypes(); // 是class还是resource
Set super Scope> getScopes(); //属于Scope:
}
其中:
TransformOutputProvider 表示 Transform 的输出,利用它我们可以获取输出路径等信息。
public interface TransformOutputProvider {
//根据 name、ContentType、QualifiedContent.Scope返回对应的文件( jar / directory)
File getContentLocation(String name, Set 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"
}
这里我们主要是做了三步处理,如下所示:
在 handleDirectory 与 handleJar 方法中则是进行了相应的 文件处理 && ASM 字节码修改。
编写完 Transform 的代码之后,我们就可以在 Plugin 的 apply 方法中加入下面代码去注册 TransformTest 的实例,代码如下所示:
参考链接:
配置构建变体 官方文档
浅析Android Gradle构建流程
Gradle Transform API 的基本使用
【Android】函数插桩(Gradle + ASM)
如何开发一款高性能的 gradle transform