傻傻分不清楚:Gradle TransformAction和AGP Transform

前言

2021年7月,Android Gradle Plugin更新了7.0版本,大家惊奇的发现,可以算得上是“家喻户晓”的Android AOP工具Transfrom标记为deprecated了,并且在注释中并未提供直接的替换类。去网上搜了搜,发现也有很多相关的文章,不过得益于国内“借鉴”的风气,我发现很多文章都不约而同的提到了一个接口:TransformAction,并在文章中声称TransformAction就是Transform的代替。真的是这样吗?这个TransformAction到底是什么?他能否完全替代Transform的功能?这篇文章就带大家一起来探究。

transform被废弃

TransformAction in Gradle

了解一个模块最好的方式就是看官方文档,点这里。当然如果嫌太长,可以先看下我的总结:TransformAction是一个使用依赖前的转换API,可以注册两个属性间的转换Action,将依赖从一个状态切换到另一个状态。
让我们从代码入手,先来看看TransformAction的声明

1
2
3
4
5
6
public interface TransformAction<T extends TransformParameters> {
@Inject
T getParameters();

void transform(TransformOutputs var1);
}

简单到爆,一共就两个方法,getParameters还被@Inject标记,不需要实现,只有一个transform方法需要实现,看名字就知道,需要在transform方法中进行转换的处理,再来一段最简单示例

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.gradle.api.artifacts.transform.TransformParameters;

public abstract class MyTransform implements TransformAction<TransformParameters.None> {
@InputArtifact
public abstract Provider<FileSystemLocation> getInputArtifact();

@Override
public void transform(TransformOutputs outputs) {
File input = getInputArtifact().get().getAsFile();
File output = outputs.file(input.getName() + ".transformed");
// Do something to generate output from input
}
}

可以看到输入的文件内容和输出的地址都可以通过框架获取。那么输入从哪里来呢?所有依赖的文件都需要作为输入被处理吗?显然不是,前文也说过,TransformAction基于依赖所具有的属性进行转换,在其注册时,提供了方法指定所需要的API。

1
2
3
4
5
6
7
// groovy code
dependencies {
registerTransform(Minify) {
from.attribute(minified, false).attribute(artifactType, "jar")
to.attribute(minified, true).attribute(artifactType, "jar")
}
}

上面的代码段展示了注册TransformAction的方式,fromto分别标记了需要处理的attribute和处理之后的attribute,符合输入条件的依赖才会被注册的TransformAction进行处理。另外,在dependencies闭包下调用注册方法也应证了TransformAction是为引入的依赖进行服务的。
因此,在使用TransformAction时需要特别注意的只有两点:1. 转换的起始状态和目标状态是什么, 2. 转换的具体实现。

举栗子

概念的介绍可能不太直观,那么举一个在Android中非常常见的例子,Android ARchive文件的使用。
Android ARchive,也就是.aar后缀的资源包,gradle是如何使用它的呢?如果有同学尝试过就知道,如果是默认使用java-libray的工程,肯定无法依赖并使用aar的,引入时会报Could not resolve ${dependencyNotation},说明在Android Gradle Plugin当中,插件对aar包的依赖进行了处理,只有通过了插件处理,才能正确使用aar内的资源。那就来看看AGP是如何在TransformAction的帮助下做到这点的。本所有代码来自Android Gradle Plugin 7.0.0。
既然aar是一个非常常见的格式,那可以尝试从这个类型的声明入手,全局搜一下"aar",在搜索列表里发现了一个重要内容

检索结果

AndroidArtifacts,注释写了:Helper for publishing android artifacts, both for internal (inter-project) and external (to repositories),可以用于帮助发布一些项目内或仓库的android文件,如其名,在类中枚举了一系列Android中可能用到的文件。再进一步看一看AAR枚举的调用点

AAR使用

调用点不多,我们可以按顺序一个个查看,当然,从名字来看,还可以再缩小定位范围,DependencyConfigurator和我们查找的aar引用看上去就有很大的关联性,那就先从这个类入手。

DepdencyConfigurator

跳转过来就发现,上面有一个registerTransform()方法的调用,追溯调用,发现最后会调用到project.dependencies.registerTransform(),而这个方法的第一个参数就是Class<? extends TransformAction<T>>,关联上了,看上去AAR这个artifact确实需要TransformAction的处理。使用AndroidArtifacts.ArtifactType.AAR的类是JetifyTransform,Jetfiy,是不是很眼熟,迁移androidx时就有jetify相关的配置,不过我们把jetfiy放到后面,先来看下有没有直接和AAR相关的TransformAction。浏览下DependencyConfigurator,找到个简单粗暴的命名:AarTransform
点进AarTransform,会有一种豁然开朗的感觉,注释写着Transform that returns the content of an extracted AAR folder(返回解压到AAR文件夹内容的Transform),还列出了一系列类型

声明

很容易就能够看出来,这里声明的是所有AAR包中可能出现的资源类型,再看下面的transform()方法

transform

也是很直接的逻辑,根据类型将aar包中的文件解压到输出目录,且输入输出都是框架提供的,使用者并不需要关心。

举栗子2

上文提到了Jetify,那这里再以Jetify举个例子。让我们先回忆一下jetifier是做什么用的——jetifier可以将引用依赖内的android.support.*引用都替换为对androidx的引用。知道了这一点,让我们来看下注册的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// com.android.build.gradle.internal.DependencyConfigurator

if (projectOptions.get(BooleanOption.ENABLE_JETIFIER)) {
registerTransform(
JetifyTransform::class.java,
AndroidArtifacts.ArtifactType.AAR,
jetifiedAarOutputType
) { params ->
params.ignoreListOption.setDisallowChanges(jetifierIgnoreList)
}
registerTransform(
JetifyTransform::class.java,
AndroidArtifacts.ArtifactType.JAR,
AndroidArtifacts.ArtifactType.PROCESSED_JAR
) { params ->
params.ignoreListOption.setDisallowChanges(jetifierIgnoreList)
}
}

第一行会读取一个属性,ENABLE_JETIFIER,是不是很熟悉,迁移androidx的时候,官方文档会要求开发者在gradle.properties文件中增加打开jetifier开关的配置,这个开关的使用场景就是这里。然后,会为aar和jar类型的依赖都注册JetifyTransform,很显然,aar文件和jar文件都会包含代码逻辑,都需要检查是否有android.support.*的引用。然后是transform()的逻辑,这个方法的注释写的非常清楚,这里就只挑重点逻辑来讲

1
2
3
4
5
6
7
8
9
10
11
// com.android.build.gradle.internal.dependency.JetifyTransform
override fun transform(transformOutputs: TransformOutputs) {
val inputFile = inputArtifact.get().asFile

val outputFile = transformOutputs.file("jetified-${inputFile.name}")
jetifierProcessor.transform2(
input = setOf(FileMapping(inputFile, outputFile)),
copyUnmodifiedLibsAlso = true,
skipLibsWithAndroidXReferences = true
)
}

简单来说,jetifier将要处理的依赖分为4类

  1. androidx的依赖库
  2. 废弃的support依赖库
  3. 被配置为忽略的依赖库
  4. 不符合上述条件的其他依赖库

1和3都不需要处理,直接忽略,2可能是未被dependency substitution替换,或者是没有相应的androidx版本,这种情况的库也会被忽略,只有符合第4点的依赖需要被处理。具体的处理逻辑在com.android.tools.build.jetifier.processor.Processor中,Processor会解压缩aar或者jar包,并将.class文件,.java文件(比如jar包中的source),.xml文件,proguard.txt文件中对android.support*的依赖都替换为响应的androidx依赖,并且如果有pom文件的话也会进行处理。完成后会将处理过的资源重新压缩,并且会带上jetified的前缀。这个前缀相信大家都很熟悉了,在更新依赖时,某些构建错误中都能发现它的身影。

一些使用的注意点

  • TransformAction在使用依赖前才会执行,并且第一次执行后会在.gradle/caches下生成缓存
  • TransformAction需要将输出写入到TransformOutputs指定的位置
  • 如果输出类型为文件时,不能通过TransformOutputs指定多个输出文件的目录,仅有第一个会生效,因此无法用它来实现Fataar的功能

Transform in Android Gradle Plugin

TransformAction介绍的差不多了,再回来聊一下Transform。”A Transform that processes intermediary build artifacts.”——注释的介绍也很直接的指出了Transform这个工具实际上用于构建中间产物的转换。而且他的实现方式也没有非常复杂,每一个新注册的Transform都会对应一系列的Task,如果开启了允许gradle在sync时构建task列表的功能,则能在Android Studio右侧的gradle列表中找到根据transform所生成的task了。
知道了Transform运行时的载体,那么Transfrom的输入输出是如何确定的呢?开发者有如何在实现逻辑时拿到自己想要处理的输入资源呢?仔细看Transform的API,会发现两个需要子类实现的方法——getScopes()getInputTypes(),从这两个方法的注释可以看出它们的返回值决定了当前Transform的输入,输出的类型则通过getOutputTypes()来确定。
最后就是如何确定资源的所属了,大家都知道,在Android构建的过程中,生成的资源也是很多的,Transform能够处理的中间产物也有很多,而为这些资源进行分类和注册是通过TransfromManager来完成的。TransformManager会将需要处理的资源集合以TransformStream的形式进行组织,并且在新增Stream的时候也会为其指明对应的ScopeContentType。以源码中注册项目中的class文件Stream的代码举例(删了一些流程无关代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// com.android.build.gradle.internal.TaskManager
protected fun addJavacClassesStream(creationConfig: ComponentCreationConfig) {
val artifacts = creationConfig.artifacts
val javaOutputs = artifacts.get(JAVAC)

// create separate streams for the output of JAVAC and for the pre/post javac
// bytecode hooks
val transformManager = creationConfig.transformManager
transformManager.addStream(
OriginalStream.builder("javac-output") // Need both classes and resources because some annotation
// processors generate resources
.addContentTypes(ImmutableSet.of(DefaultContentType.CLASSES))
.addScope(QualifiedContent.Scope.PROJECT)
.setFileCollection(project.layout.files(javaOutputs))
.build())
val variantData = creationConfig.variantData
transformManager.addStream(
OriginalStream.builder("pre-javac-generated-bytecode")
.addContentTypes(ImmutableSet.of(DefaultContentType.CLASSES))
.addScope(QualifiedContent.Scope.PROJECT)
.setFileCollection(variantData.allPreJavacGeneratedBytecode)
.build())
// post-javac-generated-bytecode
}

可以看到上面的逻辑为TransformManager添加了数个OriginalStream,第一个以”javac-output”为名的Strean,为其指定的Scope为Scope.PROJECT,ContentTypes为DefaultContentType.CLASSES,javaOutputs这个输入就是实际需要处理的资源(通过debug可以知道,这个输入实际上就是javac之后的class文件集合所在的目录,路径为/build/intermediates/javac/${variant}/classes)。第二个”pre-javac-generated-bytecode”,其实就是kotlin编译后的class文件(路径为/build/tmp/kotlin-classes/${variant},因为kotlin的编译是在java之前的,所以会归到pre这个Stream中。其他Stream的声明方式也是类似,在执行到对应Transform时,就会根据Transform需要的ScopesContentTypes选择符合条件的Stream,经过Transform的处理之后,会被消费,然后一个新的Stream会被构造出来并供下一个Transform使用。下面的流程图能够比较直观的展示stream消耗的流程

Transform Stream consume

回到Transform替换方案

大致了解了TransformActionTransform,那再让我们回到前言提到的Transform的废弃上来。虽然代码中没有举出Transform的替代方案,但是从代码中我们还是能够看出一些端倪的,从TaskManager可以找到一个新增的Task:TransformClassesWithAsmTask,他有一个即视感非常强烈的前缀,并且其中的成员也让人感觉非常熟悉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 下面两个field标记了输入(有.class文件和.jar文件),Transform中对应的就是transformInvocation.inputs
abstract val inputClassesDir: ConfigurableFileCollection

abstract val inputJarsDir: DirectoryProperty

// 这个field用于确定输出目录
abstract val inputJarsWithIdentity: JarsClasspathInputsWithIdentity

// 一些环境变量和classpath
abstract val runtimeClasspath: ConfigurableFileCollection

abstract val bootClasspath: ConfigurableFileCollection

// 输出,Transform中对应的是transformInvocation.outputProvider
abstract val classesOutputDir: DirectoryProperty

abstract val jarsOutputDir: DirectoryProperty

inputClassesDir和inputJarsDir的赋值也和TransformManager添加Stream时指定的FileCollection相同

1
2
3
4
5
6
7
8
9
inputClassesDir.from(creationConfig.artifacts.getAllClasses().filter {
!it.name.endsWith(DOT_JAR)
})

inputJarsWithIdentity.inputJars.from(
creationConfig.artifacts.getAllClasses().filter {
it.name.endsWith(DOT_JAR)
}
)

以inputClassesDir的内容举例,下面是allClasses的赋值,在AbstractAppTaskManager中,这个类是Java编写的

1
2
3
4
5
6
7
8
9
10
11
12
final Provider<Directory> javacOutput = creationConfig.getArtifacts().get(JAVAC.INSTANCE);
final FileCollection preJavacGeneratedBytecode =
creationConfig.getVariantData().getAllPreJavacGeneratedBytecode();
final FileCollection postJavacGeneratedBytecode =
creationConfig.getVariantData().getAllPostJavacGeneratedBytecode();
// 删除了一些不必要的逻辑
ConfigurableFileCollection files =
creationConfig
.getServices()
.fileCollection(
javacOutput, preJavacGeneratedBytecode, postJavacGeneratedBytecode);
creationConfig.getArtifacts().appendToAllClasses(files);

可以看到上文中提到的三个Java类编译后的相关输入,由此可见旧版的Transform和新增的TransformClassesWithAsmTask处理的源基本相同。那不同的点就在于transform这个流程了。TransformClassesWithAsmTask如其名所示,相当于强制使用了ASM进行字节码的处理(关于ASM的介绍可以参考这篇文章),并且和之前Transform使用Stream的方式不同——Stream每次被消耗后,都会通过io在build/intermediates/transforms文件夹下生成对应的输出目录,然后下一个Transform再使用之前的输出目录作为输入。TransformClassesWithAsmTask使用了visitorsList这个field维护了一个ClassVistorFactor的列表,进行transform流程时,也只需要依次应用对应的ClassVisitor即可,不需要再为每一次transform准备一个io的输出了。
那么可见,TransformClassesWithAsmTask的使用也是非常简单的,只需要注册实现的AsmClassVisitorFactory即可。官方的gradle-recipes也有AsmTransformApi的使用用例。在groovy脚本中,使用方法写出来有些类似下面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import com.android.build.api.instrumentation.*
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.util.TraceClassVisitor

androidComponents {
onVariants(selector().all(), { variant ->
variant.transformClassesWith(ExampleClassVisitorFactory, InstrumentationScope.ALL) {
it.getTransformString().set("transform params")
}
variant.setAsmFramesComputationMode(FramesComputationMode.COPY_FRAMES)
})
}

interface ExampleParams extends InstrumentationParameters {
@Input
Property<String> getTransformString()
}

abstract class ExampleClassVisitorFactory implements AsmClassVisitorFactory<ExampleParams> {
ClassVisitor createClassVisitor(
ClassContext classContext,
ClassVisitor classVisitor
) {
return new TraceClassVisitor(nextClassVisitor, PrintWriter(System.out))
}

boolean isInstrumentable(ClassData classData) {
return classData.className.startsWith("com.example")
}
}

能看到和TransformAction的注册非常像,但是实际上AsmClassVisitorFactory的使用和TransformAction并没有直接的联系,仅仅是gradle提供的objectFactory可以帮助实例化声明的接口用于参数传递。

为了兼容该做什么?

TransformClassesWithAsmTask目前其实并不灵活,绑定了ASM的使用,并且无法像Transform一样预先收集所有的依赖classpath,并且在已有的注释中也写明了当前的api并不稳定,后续可能会有较大的更新。综合来看其实并不需要着急进行新框架的兼容,等到稳定版本发布之时再考虑兼容的问题也并不迟。