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

TransformAction in Gradle
了解一个模块最好的方式就是看官方文档,点这里。当然如果嫌太长,可以先看下我的总结:TransformAction是一个使用依赖前的转换API,可以注册两个属性间的转换Action,将依赖从一个状态切换到另一个状态。
让我们从代码入手,先来看看TransformAction的声明
1 | public interface TransformAction<T extends TransformParameters> { |
简单到爆,一共就两个方法,getParameters还被@Inject标记,不需要实现,只有一个transform方法需要实现,看名字就知道,需要在transform方法中进行转换的处理,再来一段最简单示例
1 | import org.gradle.api.artifacts.transform.TransformParameters; |
可以看到输入的文件内容和输出的地址都可以通过框架获取。那么输入从哪里来呢?所有依赖的文件都需要作为输入被处理吗?显然不是,前文也说过,TransformAction基于依赖所具有的属性进行转换,在其注册时,提供了方法指定所需要的API。
1 | // groovy code |
上面的代码段展示了注册TransformAction的方式,from和to分别标记了需要处理的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枚举的调用点

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

跳转过来就发现,上面有一个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()方法

也是很直接的逻辑,根据类型将aar包中的文件解压到输出目录,且输入输出都是框架提供的,使用者并不需要关心。
举栗子2
上文提到了Jetify,那这里再以Jetify举个例子。让我们先回忆一下jetifier是做什么用的——jetifier可以将引用依赖内的android.support.*引用都替换为对androidx的引用。知道了这一点,让我们来看下注册的逻辑
1 | // com.android.build.gradle.internal.DependencyConfigurator |
第一行会读取一个属性,ENABLE_JETIFIER,是不是很熟悉,迁移androidx的时候,官方文档会要求开发者在gradle.properties文件中增加打开jetifier开关的配置,这个开关的使用场景就是这里。然后,会为aar和jar类型的依赖都注册JetifyTransform,很显然,aar文件和jar文件都会包含代码逻辑,都需要检查是否有android.support.*的引用。然后是transform()的逻辑,这个方法的注释写的非常清楚,这里就只挑重点逻辑来讲
1 | // com.android.build.gradle.internal.dependency.JetifyTransform |
简单来说,jetifier将要处理的依赖分为4类
- androidx的依赖库
- 废弃的support依赖库
- 被配置为忽略的依赖库
- 不符合上述条件的其他依赖库
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的时候也会为其指明对应的Scope和ContentType。以源码中注册项目中的class文件Stream的代码举例(删了一些流程无关代码)
1 | // com.android.build.gradle.internal.TaskManager |
可以看到上面的逻辑为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需要的Scopes和ContentTypes选择符合条件的Stream,经过Transform的处理之后,会被消费,然后一个新的Stream会被构造出来并供下一个Transform使用。下面的流程图能够比较直观的展示stream消耗的流程

回到Transform替换方案
大致了解了TransformAction和Transform,那再让我们回到前言提到的Transform的废弃上来。虽然代码中没有举出Transform的替代方案,但是从代码中我们还是能够看出一些端倪的,从TaskManager可以找到一个新增的Task:TransformClassesWithAsmTask,他有一个即视感非常强烈的前缀,并且其中的成员也让人感觉非常熟悉
1 | // 下面两个field标记了输入(有.class文件和.jar文件),Transform中对应的就是transformInvocation.inputs |
inputClassesDir和inputJarsDir的赋值也和TransformManager添加Stream时指定的FileCollection相同
1 | inputClassesDir.from(creationConfig.artifacts.getAllClasses().filter { |
以inputClassesDir的内容举例,下面是allClasses的赋值,在AbstractAppTaskManager中,这个类是Java编写的
1 | final Provider<Directory> javacOutput = creationConfig.getArtifacts().get(JAVAC.INSTANCE); |
可以看到上文中提到的三个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 | import com.android.build.api.instrumentation.* |
能看到和TransformAction的注册非常像,但是实际上AsmClassVisitorFactory的使用和TransformAction并没有直接的联系,仅仅是gradle提供的objectFactory可以帮助实例化声明的接口用于参数传递。
为了兼容该做什么?
TransformClassesWithAsmTask目前其实并不灵活,绑定了ASM的使用,并且无法像Transform一样预先收集所有的依赖classpath,并且在已有的注释中也写明了当前的api并不稳定,后续可能会有较大的更新。综合来看其实并不需要着急进行新框架的兼容,等到稳定版本发布之时再考虑兼容的问题也并不迟。