前言

目前,使用 AGP Transform API 进行字节码插桩已经非常普遍了,例如 Booster、神策等框架中都有 Transform 的影子。Transform 听起来很高大上,其本质就是一个 Gradle Task。在这篇文章里,我将带你理解 Transform 的工作机制、使用方法和核心源码解析,并通过一个 Demo 帮助你融会贯通。

这篇文章是全面掌握 Gradle 构建系统系列的第 4 篇:

请点赞加关注,你的支持对我非常重要,满足下我的虚荣心。

Hi,我是小彭。本文已收录到 GitHub · Android-NoteBook 中。这里有 Android 进阶成长知识体系,有志同道合的朋友,欢迎跟着我一起成长。(联系方式在 GitHub)


1. 认识 Transform

1.1 什么是 Transform?

Transform API 是 Android Gradle Plugin 1.5 就引入的特性,主要用于在 Android 构建过程中,在 Class→Dex 这个节点修改 Class 字节码。利用 Transform API,我们可以拿到所有参与构建的 Class 文件,借助 Javassist 或 ASM 等字节码编辑工具进行修改,插入自定义逻辑。一般来说,这些自定义逻辑是与业务逻辑无关的。

使用 Transform 的常见的应用场景有:

  • 埋点统计: 在页面展现和退出等生命周期中插入埋点统计代码,以统计页面展现数据;
  • 耗时监控: 在指定方法的前后插入耗时计算,以观察方法执行时间;
  • 方法替换: 将方法调用替换为调用另一个方法。

1.2 Transform 的基本原理

先大概了解下 Transform 的工作机制:

  • 1、工作时机: Transform 工作在 Android 构建中由 Class → Dex 的节点;
  • 2、处理对象: 处理对象包括 Javac 编译后的 Class 文件、Java 标准 resource 资源、本地依赖和远程依赖的 JAR/AAR。Android 资源文件不属于 Transform 的操作范围,因为它们不是字节码;
  • 3、Transform Task: 每个 Transform 都对应一个 Task,Transform 的输入和输出可以理解为对应 Transform Task 的输入输出。每个 TransformTask 的输出都分别存储在 app/build/intermediates/transform/[Transform Name]/[Variant] 文件夹中;
  • 4、Transform 链: TaskManager 会将每个 TransformTask 串联起来,前一个 Transform 的输出会作为下一个 Transform 的输入。

1.3 Transform API

了解 Transform 的基本工作机制后,我们先来看 Transform 的核心 API。这里仅列举出 Transform 抽象类中最核心的方法,有几个次要的方法后面再说。

com.android.build.api.transform.java

public abstract class Transform {

    // 指定 Transform 的名称,该名称还会用于组成 Task 的名称
// 格式为 transform[InputTypes]With[name]For[Configuration]
public abstract String getName(); // (孵化中)用于过滤 Variant,返回 false 表示该 Variant 不执行 Transform
public boolean applyToVariant(VariantInfo variant) {
return true;
} // 指定输入内容类型
public abstract Set<ContentType> getInputTypes(); // 指定输出内容类型,默认取 getInputTypes() 的值
public Set<ContentType> getOutputTypes() {
return getInputTypes();
} // 指定消费型输入内容范畴
public abstract Set<? super Scope> getScopes(); // 指定引用型输入内容范畴
public Set<? super Scope> getReferencedScopes() {
return ImmutableSet.of();
} // 指定是否支持增量编译
public abstract boolean isIncremental(); // 核心 API
public void transform(TransformInvocation transformInvocation)
throws TransformException, InterruptedException, IOException {
// 分发到过时 API,以兼容旧版本的 Transform
//noinspection deprecation
transform(transformInvocation.getContext(), transformInvocation.getInputs(),
transformInvocation.getReferencedInputs(),
transformInvocation.getOutputProvider(),
transformInvocation.isIncremental());
} // 指定是否支持缓存
public boolean isCacheable() {
return false;
}
}

1.4 ContentType 内容类型

ContentType 是一个枚举类接口,表示输入或输出内容的类型,在 AGP 中定义了 DefaultContentTypeExtendedContentType 两个枚举类。但是,我们在自定义 Transform 时只能使用 DefaultContentType 中定义的枚举,即 CLASSESRESOURCES 两种类型,其它类型仅供 AGP 内置的 Transform 使用。

自定义 Transform 需要在两个位置定义内容类型:

  • 1、Set getInputTypes(): 指定输入内容类型,允许通过 Set 集合设置输入多种类型;
  • 2、Set getOutputTypes(): 指定输出内容类型,默认取 getInputTypes() 的值,允许通过 Set 集合设置输出多种类型。

ExtendedContentType.java

// 加强类型,自定义 Transform 无法使用
public enum ExtendedContentType implements ContentType { // DEX 文件
DEX(0x1000), // Native 库
NATIVE_LIBS(0x2000), // Instant Run 加强类
CLASSES_ENHANCED(0x4000), // Data Binding 中间产物
DATA_BINDING(0x10000), // Dex Archive
DEX_ARCHIVE(0x40000),
;
}

QualifiedContent.java

enum DefaultContentType implements ContentType {

    // Java 字节码,包括 Jar 文件和由源码编译产生的
CLASSES(0x01), // Java 资源
RESOURCES(0x02);
}

在 TransformManager 中,预定义了一部分内容类型集合,常用的是 CONTENT_CLASS 操作 Class。

TransformManager.java

public static final Set<ContentType> CONTENT_CLASS = ImmutableSet.of(CLASSES);
public static final Set<ContentType> CONTENT_JARS = ImmutableSet.of(CLASSES, RESOURCES);
public static final Set<ContentType> CONTENT_RESOURCES = ImmutableSet.of(RESOURCES);

1.5 ScopeType 作用域

ScopeType 也是一个枚举类接口,表示输入内容的范畴。在 AGP 中定义了 InternalScopeScope 两个枚举类。但是,我们在自定义 Transform 只能使用 Scope 中定义的枚举,其它类型仅供 AGP 内置的 Transform 使用。

Transform 需要在两个位置定义输入内容范围:

  • 1、Set getScopes() 消费型输入内容范畴: 此范围的内容会被消费,因此当前 Transform 必须将修改后的内容复制到 Transform 的中间目录中,否则无法将内容传递到下一个 Transform 处理;
  • 2、Set getReferencedScopes() 指定引用型输入内容范畴: 默认是空集合,此范围的内容不会被消费,因此不需要复制传递到下一个 Transform,也不允许修改。

InternalScope.java

// 内部使用的作用域,自定义 Transform 无法使用
public enum InternalScope implements QualifiedContent.ScopeType { MAIN_SPLIT(0x10000), LOCAL_DEPS(0x20000), FEATURES(0x40000),
;
}

QualifiedContent.java

enum Scope implements ScopeType {

    // 当前模块
PROJECT(0x01),
// 子模块
SUB_PROJECTS(0x04),
// 外部依赖,包括当前模块和子模块本地依赖和远程依赖的 JAR/AAR
EXTERNAL_LIBRARIES(0x10),
// 当前变体所测试的代码(包括依赖项)
TESTED_CODE(0x20),
// 本地依赖和远程依赖的 JAR/AAR(provided-only)
PROVIDED_ONLY(0x40),
}

在 TransformManager 中,预定义了一部分作用域集合,常用的是 SCOPE_FULL_PROJECT 所有模块。需要注意,Library 模块注册的 Transform 只能使用 Scope.PROJECT。

TransformManager.java

public static final Set<ScopeType> PROJECT_ONLY = ImmutableSet.of(Scope.PROJECT);
public static final Set<ScopeType> SCOPE_FULL_PROJECT = ImmutableSet.of(Scope.PROJECT, Scope.SUB_PROJECTS, Scope.EXTERNAL_LIBRARIES);

1.6 transform 方法

transform() 是实现 Transform 的核心方法,方法的参数是 TransformInvocation,它提供了所有与输入输出相关的信息:

public interface TransformInvocation {

    Context getContext();

    // 消费型输入内容
Collection<TransformInput> getInputs(); // 引用型输入内容
Collection<TransformInput> getReferencedInputs(); // 额外输入内容
Collection<SecondaryInput> getSecondaryInputs(); // 输出信息
TransformOutputProvider getOutputProvider(); // 是否增量构建
boolean isIncremental();
}
  • isIncremental(): 当前 Transform 任务是否增量构建;
  • getInputs(): 获取 TransformInput 对象,它是消费型输入内容,对应于 Transform#getScopes() 定义的范围;
  • getReferencedInputs(): 获取 TransformInput 对象,它是引用型输入内容,对应于 Transform#getReferenceScope() 定义的内容范围;
  • getOutPutProvider(): TransformOutputProvider 是对输出文件的抽象。

输入内容 TransformInput 由两部分组成:

  • DirectoryInput 集合: 以源码方式参与构建的输入文件,包括完整的源码目录结构及其中的源码文件;
  • JarInput 集合: 以 Jar 和 aar 依赖方式参与构建的输入文件,包含本地依赖和远程依赖。

输入内容信息 TransformOutputProvider 有两个功能:

  • deleteAll(): 当 Transform 运行在非增量构建模式时,需要删除上一次构建产生的所有中间文件,可以直接调用 deleteAll() 完成;
  • getContentLocation(): 获得指定范围+类型的输出目标路径。

TransformOutputProvider.java

public interface TransformOutputProvider {

    // 删除所有中间文件
void deleteAll() // 获取指定范围+类型的目标路径
File getContentLocation(String name,
Set<QualifiedContent.ContentType> types,
Set<? super QualifiedContent.Scope> scopes,
Format format);
}

获取输入内容对应的输出路径:

for (input in transformInvocation.inputs) {
for (jarInput in input.jarInputs) {
// 输出路径
val outputJar = outputProvider.getContentLocation(
jarInput.name,
jarInput.contentTypes,
jarInput.scopes,
Format.JAR
)
}
}

1.7 Transform 增量模式

任何构建系统都会尽量避免重复执行相同工作,Transform 也不例外。虽然增量构建并不是必须的,但作为一个合格的 Transform 实现应该具备增量能力。

1、增量模式标记位: Transform API 有两个增量标志位,不要混淆:

  • Transform#isIncremental(): Transform 增量构建的使能开关,返回 true 才有可能触发增量构建;
  • TransformInvocation#isIncremental(): 当次 TransformTask 是否增量执行,返回 true 表示正在增量模式。

2、Task 增量模式与 Transform 增量模式的区别: Task 增量模式与 Transform 增量模式的区别在于,Task 增量执行时会跳过整个 Task 的动作列表,而 Transform 增量执行依然会执行 TransformTask,但输入内容会增加变更内容信息。

3、增量模式的输入: 增量模式下的所有输入都是带状态的,需要根据这些状态来做不同的处理,不需要每次所有流程都重新来一遍。比如新增的输入就需要处理,而未修改的输入就不需要处理。Transform 定义了四个输入文件状态:

com.android.build.api.transform.Status.java

public enum Status {

    // 未修改,不需要处理,也不需要复制操作
NOTCHANGED, // 新增,正常处理并复制给下一个任务
ADDED, // 已修改,正常处理并复制给下一个任务
CHANGED, // 已删除,需同步移除 OutputProvider 指定的目标文件
REMOVED;
}

1.8 注册 Transform

在 BaseExtension 中维护了一个 Transform 列表,自定义 Transform 需要注册才能生效,而且还支持额外设置 TransformTask 的依赖。

BaseExtension.kt

abstract class BaseExtension {
private val _transforms: MutableList<Transform> = mutableListOf()
private val _transformDependencies: MutableList<List<Any>> = mutableListOf()
... fun registerTransform(transform: Transform, vararg dependencies: Any) {
_transforms.add(transform)
_transformDependencies.add(listOf(dependencies))
}
}

注册 Transform:

// 获取 Android 扩展
val androidExtension = project.extensions.getByType(BaseExtension::class.java)
// 注册 Transform,支持额外增加依赖
androidExtension.registerTransform(ToastTransform(project)/* 支持增加依赖*/)

提示: 为了提高编译效率,可以判断 Variant 为 release 类型才注册 Transform,也可以通过重写 Transform#applyToVariant() 来决定是否执行 Transform。


2. Transform 核心源码分析

这一节我们来分析 Transform 相关核心源码,这里我们引用的是 Android Gradle Plugin 7.1.0 版本的源码。

2.1 Transform 与 Task 的关系

Project 的构建逻辑由一系列 Task 的组成,每个 Task 负责完成一个基本的工作,例如 Javac 编译 Task。Transform 也是依靠 Task 执行的,在配置阶段,Gradle 会为注册的 Transform 创建对应的 Task。

提示: 说 “创建” 可能不太严谨,TransformManager 使用 register 懒创建的方式注册 Task,其实还没有创建 Task 实例。我们不要复杂化了,就说创建吧。

而 Task 的依赖关系是通过 TransformTask 的输入输出关系隐式确定的,TransformManager 通过 TransformStream 链接各个 TransformTask 的输入输出,进而控制 Transform 的依赖关系顺序。

LibraryTaskManager.java

@Override
protected void doCreateTasksForVariant(ComponentInfo<LibraryVariantBuilderImpl, LibraryVariantImpl> variantInfo) {
...
// ----- External Transforms -----
// apply all the external transforms.
List<Transform> customTransforms = extension.getTransforms();
List<List<Object>> customTransformsDependencies = extension.getTransformsDependencies(); final IssueReporter issueReporter = libraryVariant.getServices().getIssueReporter(); for (int i = 0, count = customTransforms.size(); i < count; i++) {
Transform transform = customTransforms.get(i); // Check the transform only applies to supported scopes for libraries:
// We cannot transform scopes that are not packaged in the library
// itself.
Sets.SetView<? super Scope> difference = Sets.difference(transform.getScopes(), TransformManager.PROJECT_ONLY);
if (!difference.isEmpty()) {
String scopes = difference.toString();
issueReporter.reportError(
Type.GENERIC,
String.format(
"Transforms with scopes '%s' cannot be applied to library projects.",
scopes));
} List<Object> deps = customTransformsDependencies.get(i);
transformManager.addTransform(
taskFactory,
libraryVariant,
transform,
null,
task -> {
// (3.2节提到的额外依赖)
// 在注册 Transform 时,可以额外增加依赖
if (!deps.isEmpty()) {
task.dependsOn(deps);
}
},
taskProvider -> {
// if the task is a no-op then we make assemble task
// depend on it.
if (transform.getScopes().isEmpty()) {
TaskFactoryUtils.dependsOn(
libraryVariant.getTaskContainer().getAssembleTask(),
taskProvider);
}
});
} // Create jar with library classes used for publishing to runtime elements.
taskFactory.register(new BundleLibraryClassesJar.CreationAction(
libraryVariant, AndroidArtifacts.PublishedConfigType.RUNTIME_ELEMENTS));
...
}

网上很多朋友提到 “自定义 Transform 的执行时机早于系统内置 Transform”,但从 AGP 7.1.0 源码看,并不存在系统 Transform。猜测是新版本 AGP 将这部分 “系统内置 Transform” 修改为由 Task 直接实现,毕竟 从 AGP 7.0 开始 Transform 标记为过时了。

2.2 Transform 的创建过程

  • 1、注册 Transform: 注册 Transform 仅是将对象注册到 BaseExtension 中的列表中。TransformManager 会通过 Task 的输入输出隐式建立 Transform 的依赖顺序,另外还支持在注册时添加额外的依赖。

BaseExtension.kt

abstract class BaseExtension {
private val _transforms: MutableList<Transform> = mutableListOf()
private val _transformDependencies: MutableList<List<Any>> = mutableListOf()
... fun registerTransform(transform: Transform, vararg dependencies: Any) {
_transforms.add(transform)
_transformDependencies.add(listOf(dependencies))
}
}
  • 2、创建 TransformTask 的执行链: TransformTask 属于 Android 构建构成的一部分,所有 Android Task 的创建入口都从 BasePlugin#createAndroidTasks() 开始。其中会为所有 Variant 变体创建相关的 Task,经过一系列调用后,会通过抽象方法 TaskManager#doCreateTaskForVariant() 分派到 ApplicationTaskManager 和 LibraryTaskManager 两个子类中,以区分 App 模块和 Library 模块。

调用链概要:

BasePlugin#createAndroidTasks()
-> TaskManager#createTasks()->遍历所有变体
-> for {
TaskManager#createTasksForVariant(variant)
-> abstract TaskManager#doCreateTasksForVariant(variant)
// App
-> ApplicationTaskManager#doCreateTasksForVariant(variant)
-> ApplicationTaskManager#createCommonTask(variant)
-> ApplicationTaskManager#createCompileTask(variant)
-> TaskManager#createPostCompilationTasks(config)
-> for { Transform#addTransform(transform) }
// Library
-> LibraryTaskManager#doCreateTasksForVariant(variant)
-> for { Transform#addTransform(transform) }
}

2.3 TransformTask 的命名格式

Transform#getName() 会用于构造 Task Name,命名格式为 transform[InputTypes]With[name]For[Configuration],例如 transformClassed。这块源码体现在 TransformManager 中创建 Task 的位置:

TransformManager.java

// 创建 Transform Task
public <T extends Transform> Optional<TaskProvider<TransformTask>> addTransform(...) {
...
// TaskName = 前缀 + Configuration
String taskName = creationConfig.computeTaskName(getTaskNamePrefix(transform), "");
...
} // TaskName 前缀
static String getTaskNamePrefix(Transform transform) {
StringBuilder sb = new StringBuilder(100);
sb.append("transform");
sb.append(transform
.getInputTypes()
.stream()
.map(inputType -> CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, inputType.name()))
.sorted() // Keep the order stable.
.collect(Collectors.joining("And")));
sb.append("With");
StringHelper.appendCapitalized(sb, transform.getName());
sb.append("For"); return sb.toString();
}

2.4 TransformTask 的输入输出

TransformTask 通过 @Input 和 @OutputDirectory 等注解,将 Transform API 关联到 Task 的输入输出上:

TransformTask.java

public abstract class TransformTask extends StreamBasedTask {

    ...

    @Input
public Set<QualifiedContent.ContentType> getInputTypes() {
return transform.getInputTypes();
} @OutputDirectory
@Optional
public abstract DirectoryProperty getOutputDirectory();
}

2.5 执行 transform() 方法

每个 Task 内部都保持了一个 Action 列表 actions,执行 Task 就是按顺序执行这个列表,对于自定义 Task,可以通过 @TaskAction 注解添加默认 Action。

TransformTask.java

@TaskAction
void transform(final IncrementalTaskInputs incrementalTaskInputs) {
...
transform.transform(new TransformInvocationBuilder(context)
.addInputs(consumedInputs.getValue())
.addReferencedInputs(referencedInputs.getValue())
.addSecondaryInputs(changedSecondaryInputs.getValue())
.addOutputProvider(outputStream != null
? outputStream.asOutput()
: null)
.setIncrementalMode(isIncremental.getValue())
.build());
...
}

2.6 Library 模块限制

Library 模块仅只支持使用 Scope.PROJECT 作用域:

LibraryTaskManager.java

// Check the transform only applies to supported scopes for libraries:
// We cannot transform scopes that are not packaged in the library
// itself.
Sets.SetView<? super Scope> difference = Sets.difference(transform.getScopes(), TransformManager.PROJECT_ONLY);
if (!difference.isEmpty()) {
String scopes = difference.toString();
issueReporter.reportError(Type.GENERIC, String.format("Transforms with scopes '%s' cannot be applied to library projects.",scopes));
}

3. 自定义 Transform 模板

上一节我们探讨了 Transform 的基本工作机制,第 3 节和第 4 节我们来实现一个 Transform Demo。Transform 的核心代码在 transform() 方法中,我们要做的就是遍历输入文件,再把修改后的文件复制到目标路径中,对于 JarInputs 还有一次解压和压缩。更进一步,再考虑增量编译的情况。

因此,整个 Transform 的核心过程是有固定套路,模板流程图如下:

—— 图片引用自 https://rebooters.github.io/2020/01/04/Gradle-Transform-ASM-探索/

我们把整个流程图做成一个抽象模板类,子类需要重写 provideFunction() 方法,从输入流读取 Class 文件,修改完字节码后再写入到输出流。甚至不需要考虑 Trasform 的输入文件遍历、加解压、增量等,舒服!

BaseCustomTransform.kt

abstract class BaseCustomTransform(private val debug: Boolean) : Transform() {

    abstract fun provideFunction(): ((InputStream, OutputStream) -> Unit)?

    open fun classFilter(className: String) = className.endsWith(SdkConstants.DOT_CLASS)

    override fun isIncremental() = true

    override fun transform(transformInvocation: TransformInvocation) {
super.transform(transformInvocation) log("Transform start, isIncremental = ${transformInvocation.isIncremental}.") val inputProvider = transformInvocation.inputs
val referenceProvider = transformInvocation.referencedInputs
val outputProvider = transformInvocation.outputProvider // 1. Transform logic implemented by subclasses.
val function = provideFunction() // 2. Delete all transform tmp files when not in incremental build.
if (!transformInvocation.isIncremental) {
log("All File deleted.")
outputProvider.deleteAll()
} for (input in inputProvider) {
// 3. Transform jar input.
log("Transform jarInputs start.")
for (jarInput in input.jarInputs) {
val inputJar = jarInput.file
val outputJar = outputProvider.getContentLocation(jarInput.name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
if (transformInvocation.isIncremental) {
// 3.1 Transform jar input in incremental build.
when (jarInput.status ?: Status.NOTCHANGED) {
Status.NOTCHANGED -> {
// Do nothing.
}
Status.ADDED, Status.CHANGED -> {
// Do transform.
transformJar(inputJar, outputJar, function)
}
Status.REMOVED -> {
// Delete.
FileUtils.delete(outputJar)
}
}
} else {
// 3.2 Transform jar input in full build.
transformJar(inputJar, outputJar, function)
}
}
// 4. Transform dir input.
log("Transform dirInput start.")
for (dirInput in input.directoryInputs) {
val inputDir = dirInput.file
val outputDir = outputProvider.getContentLocation(dirInput.name, dirInput.contentTypes, dirInput.scopes, Format.DIRECTORY)
if (transformInvocation.isIncremental) {
// 4.1 Transform dir input in incremental build.
for ((inputFile, status) in dirInput.changedFiles) {
val outputFile = concatOutputFilePath(outputDir, inputFile)
when (status ?: Status.NOTCHANGED) {
Status.NOTCHANGED -> {
// Do nothing.
}
Status.ADDED, Status.CHANGED -> {
// Do transform.
doTransformFile(inputFile, outputFile, function)
}
Status.REMOVED -> {
// Delete
FileUtils.delete(outputFile)
}
}
}
} else {
// 4.2 Transform dir input in full build.
for (inputFile in FileUtils.getAllFiles(inputDir)) {
// Traversal fileTree (depthFirstPreOrder).
if (classFilter(inputFile.name)) {
val outputFile = concatOutputFilePath(outputDir, inputFile)
doTransformFile(inputFile, outputFile, function)
}
}
}
}
}
log("Transform end.")
} /**
* Do transform Jar.
*/
private fun transformJar(inputJar: File, outputJar: File, function: ((InputStream, OutputStream) -> Unit)?) {
// Create parent directories to hold outputJar file.
Files.createParentDirs(outputJar)
// Unzip.
FileInputStream(inputJar).use { fis ->
ZipInputStream(fis).use { zis ->
// Zip.
FileOutputStream(outputJar).use { fos ->
ZipOutputStream(fos).use { zos ->
var entry = zis.nextEntry
while (entry != null && isValidZipEntryName(entry)) {
if (!entry.isDirectory && classFilter(entry.name)) {
zos.putNextEntry(ZipEntry(entry.name))
// Apply transform function.
applyFunction(zis, zos, function)
}
entry = zis.nextEntry
}
}
}
}
}
} /**
* Do transform file.
*/
private fun doTransformFile(inputFile: File, outputFile: File, function: ((InputStream, OutputStream) -> Unit)?) {
// Create parent directories to hold outputFile file.
Files.createParentDirs(outputFile)
FileInputStream(inputFile).use { fis ->
FileOutputStream(outputFile).use { fos ->
// Apply transform function.
applyFunction(fis, fos, function)
}
}
} private fun concatOutputFilePath(outputDir: File, inputFile: File) = File(outputDir, inputFile.name) private fun applyFunction(input: InputStream, output: OutputStream, function: ((InputStream, OutputStream) -> Unit)?) {
try {
if (null != function) {
function.invoke(input, output)
} else {
// Copy
input.copyTo(output)
}
} catch (e: UncheckedIOException) {
throw e.cause!!
}
} private fun log(logStr: String) {
if (debug) {
println("$name - $logStr")
}
}
}

4. Hello Transform 示例

现在,我手把手带你基于 BaseCustomTransform 实现一个 Transform Demo。示例代码我已经上传到 Github · DemoHall · HelloTransform。有用请给个免费的 Star 支持下。

Demo 效果很简单:

  • 实现一个 Transform,在编译时在 Activity#onCreate() 方法末尾织入一个 Toast 语句;
  • 仅通过自定义注解 @Hello 修饰的 Activity#onCreate() 方法会生效。

4.1 步骤 1:初始化代码框架

首先,我们先搭建工程的整体框架,再来编写核心的 Transform 逻辑。我们选择自定义 Gradle 插件来承载 Transform 的逻辑,可维护性更好。关于自定义 Gradle 插件的步骤具体见上一篇文章《手把手带你自定义 Gradle 插件》,此处不展开。

提示: 提醒一下,并不是说一定要由 Gradle 插件来承载,你直接在 .gradle 文件中实现也是 OK 的。

插件实现类如下:

ToastPlugin.kt

class ToastPlugin : Plugin<Project> {
override fun apply(project: Project) {
// 获取 Android 扩展
val androidExtension = project.extensions.getByType(BaseExtension::class.java)
// 注册 Transform,支持额外增加依赖
androidExtension.registerTransform(ToastTransform(project)/* 支持增加依赖*/)
}
}

4.2 步骤 2:拷贝 Transform 模板类

将我们实现的 BaseCustomTransform 模板类复制到工程下,再实现一个子类:

ToastTransform.kt

internal class ToastTransform(val project: Project) : BaseCustomTransform(true) {

    // Transform 名
override fun getName() = "ToastTransform" // 是否支持增量构建
override fun isIncremental() = true /**
* 用于过滤 Variant,返回 false 表示该 Variant 不执行 Transform
*/
@Incubating
override fun applyToVariant(variant: VariantInfo?): Boolean {
return "debug" == variant?.buildTypeName
} // 指定输入内容类型
override fun getInputTypes() = TransformManager.CONTENT_CLASS // 指定消费型输入内容范畴
override fun getScopes() = TransformManager.SCOPE_FULL_PROJECT // 转换方法
override fun provideFunction() = { ios: InputStream, zos: OutputStream ->
input.copyTo(output)
}
}

其中,provideFunction() 是模板代码,参数分别表示源 Class 文件的输入流和目标 Class 文件输出流。子类要做的事,就是从输入流读取 Class 信息,修改后写入到输出流。

4.3 步骤 3:使用 Javassist 修改字节码

使用 Javassist API 从输入流加载数据,在匹配到 onCreate() 方法后检查是否声明 @Hello 注解。是则在该方法末尾织入一句 Toast:Hello Transform。本文重点不是 Javassist,此处就不展开了。

override fun provideFunction() = { ios: InputStream, zos: OutputStream ->
val classPool = ClassPool.getDefault()
// 加入android.jar
classPool.appendClassPath((project.extensions.getByName("android") as BaseExtension).bootClasspath[0].toString())
classPool.importPackage("android.os.Bundle")
// Input
val ctClass = classPool.makeClass(ios)
try {
ctClass.getDeclaredMethod("onCreate").also {
println("onCreate found in ${ctClass.simpleName}")
val attribute = it.methodInfo.getAttribute(AnnotationsAttribute.invisibleTag) as? AnnotationsAttribute
if (null != attribute?.getAnnotation("com.pengxr.hellotransform.Hello")) {
println("Insert toast in ${ctClass.simpleName}")
it.insertAfter(
"""android.widget.Toast.makeText(this,"Hello Transform!",android.widget.Toast.LENGTH_SHORT).show();
"""
)
}
}
} catch (e: NotFoundException) {
// ignore
}
// Output
zos.write(ctClass.toBytecode())
ctClass.detach()
}

4.4 步骤 4:应用插件

sample 模块 build.gradle

apply plugin: 'com.pengxr.toastplugin'

4.5 步骤 5:声明 @Hello 注解

HelloActivity.kt

class HelloActivity : AppCompatActivity() {

    @Hello
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_hello)
}
}

4.6 步骤 6:运行

完成以上步骤后,编译运行程序。可以在 Build Output 看到以下输出,HelloActivity 启动时会弹出 Toast HelloTransform,说明织入成功。

...
Task :sample:mergeDebugJavaResource > Task :sample:transformClassesWithToastTransformForDebug
...
onCreate found in HelloActivity
Insert toast in HelloActivity
ToastTransform - Transform end. > Task :sample:dexBuilderDebug
> Task :sample:mergeExtDexDebug
> Task :sample:mergeDexDebug
> Task :sample:packageDebug
> Task :sample:createDebugApkListingFileRedirect
> Task :sample:assembleDebug BUILD SUCCESSFUL in 3m 18s
33 actionable tasks: 33 executed Build Analyzer results available

5. Transform 的未来

从 AGP 7.0 开始,Transform API 已经被废弃了。是的,就是卷,而且这次直接是降维打击。以前 Transform 是 AGP 的特性,现在 Gradle 也来整 Transform,不过换了个名字,叫 —— TransformAction

那么,我们还有必要学 AGP Transform API 吗?如果你现在涉足字节码插桩这块,你建议你还是学以下:

  • 1、社区沉淀: AGP Transform API 发展多年,目前社区中已经沉淀下非常多优秀的开源组件和博客,这些资源对你非常有帮助。而 TransformAction 的社区沉淀还非常单薄;
  • 2、技术思维: 虽然换了一套 API,但背后的思路 / 套路是相似的。理解 AGP Transform 的工作机制,对你理解 Gradle TransformAction 有事半功倍的效果。

例如,以下是 Gradle 官方文档的演示代码,是不是套路差不多?

abstract class CountLoc implements TransformAction<TransformParameters.None> {

    @Inject
abstract InputChanges getInputChanges() @PathSensitive(PathSensitivity.RELATIVE)
@InputArtifact
abstract Provider<FileSystemLocation> getInput() @Override
void transform(TransformOutputs outputs) {
def outputDir = outputs.dir("${input.get().asFile.name}.loc")
println("Running transform on ${input.get().asFile.name}, incremental: ${inputChanges.incremental}")
inputChanges.getFileChanges(input).forEach { change ->
def changedFile = change.file
if (change.fileType != FileType.FILE) {
return
}
def outputLocation = new File(outputDir, "${change.normalizedPath}.loc")
switch (change.changeType) {
case ADDED:
case MODIFIED:
println("Processing file ${changedFile.name}")
outputLocation.parentFile.mkdirs() outputLocation.text = changedFile.readLines().size() case REMOVED:
println("Removing leftover output file ${outputLocation.name}")
outputLocation.delete() }
}
}
}

6. 总结

本文的示例代码已上传到 https://github.com/pengxurui/DemoHall,请 Star 支持。关注我,带你了解更多,我们下次见。

参考资料

你的点赞对我意义重大!微信搜索公众号 [彭旭锐],希望大家可以一起讨论技术,找到志同道合的朋友,我们下次见!

其实 Gradle Transform 就是个纸老虎 —— Gradle 系列(4)的更多相关文章

  1. 第二篇:gradle脚本运行环境分析(gradle的语义模型)

    引言:通过上一篇的论述,我们知道gradle脚本是如假包换的groovy代码,但是这个groovy代码是运行在他的上下文环境里面的,学名叫语义模型.这一篇我们就来看看他的语义模型到底是什么,如何使用. ...

  2. [Gradle] 在 Eclipse 下利用 gradle 构建系统

      转载自:http://www.ibm.com/developerworks/cn/opensource/os-cn-gradle/ 构建系统时候常常要用到 Ant, Maven 等工具,对于初学者 ...

  3. gradle入门(1-1)gradle的概念和使用

    一.Gradle是什么 Gradle是一种Java应用构建工具,它采用领域特定语言 Groovy 语法实现配置. 1.Gradle的基本概念 项目:项目的配置 即 build.gradle. 任务:任 ...

  4. Gradle安装 Gradle效率提升 eclipse安装gradle插件 【我】

    Gradle安装 从官网下载 gradle4.6版本,也可以从svn地址下载 https://downloads.gradle.org/distributions/gradle-4.6-bin.zip ...

  5. Eclipse集成Gradle 【Eclipse在线安装Gradle插件方法】

    本章将介绍了Eclipse集成Gradle.以下是将Gradle插件添加到Eclipse的步骤. 步骤1 - 打开Eclipse Marketplace 打开在系统中安装好的Eclipse. 转到 J ...

  6. eclipse gradle插件 org.gradle.tooling.GradleConnectionException: Could not install Gradle distribution from 'https://services.gradle.org/distributions/gradle-3.4-bin.zip'.

    eclipse安装gradle后出现如下异常: org.gradle.tooling.GradleConnectionException: Could not install Gradle distr ...

  7. Gradle修改缓存路径 和 Gradle修改Maven仓库地址

    Gradle修改缓存路径 修改gradle启动脚本进入gradle安装的bin目录,使用文本编辑器打开gradle.bat文件,在如图的位置添加以下语句 set GRADLE_OPTS="- ...

  8. Gradle sync failed: Cause: org.gradle.logging.StyledTextOutput$Style Consult IDE log for more details

    环境 Android studio 3.0 导入开源中国: ... dependencies { //noinspection GradleDependency classpath 'com.andr ...

  9. 创建Gradle工程出现Could not install Gradle distribution from 'https://services.gradle.org/distributions/gradleXX'.问题解决

    在 Eclipse EE Oxygen 中创建 Gradle Project的时候 出现如下错误: org.gradle.tooling.GradleConnectionException: Coul ...

随机推荐

  1. 深入Linux 内核架构之 CFS

    linux内核分析--CFS(完全公平调度算法)   1.1 CFS原理 cfs定义了一种新的模型,它给cfs_rq(cfs的run queue)中的每一个进程安排一个虚拟时钟,vruntime.如果 ...

  2. ros中关于节点、话题、服务以及自定义消息等在终端中的常用命令

    以下面的计算力图说明 节点相关常用命令 在终端中查看项目中有哪些节点命令:rosnode list 有了节点信息想要查看节点中到底发布订阅了哪些话题,作为服务端服务类型或者作为客户端需要的服务类型以上 ...

  3. 什么是arduino及.arduino分类

    关于什么是arduino没有什么可说的,想要接触arduino多多少少的都会对arduino有一定的理解,我认为,arduino应该算是一个连接硬件与软件的平台,通过他,你可以将你的想法以代码的形式呈 ...

  4. PCB基础知识(一)

    在电子行业有一个关键的部件叫做PCB(printed circuit board,印刷电路板).这是一个太基础的部件,导致很多人都很难解释到底什么是PCB.这篇文章将会详细解释PCB的构成,以及在PC ...

  5. 阿里云场景化阿里云企业数字化转型售前方法PSA

    阿里云场景化阿里云企业数字化转型售前方法PSA 目录 01 课程收获 理解企业数字化转型的概念.内涵.本质 了解企业数字化转型的要点.目标和切入点 掌握数字化转型项目售前阶段实践方法 场景化方案 阿里 ...

  6. 对height 100%和inherit的总结

    对height 100%和inherit的总结 欢迎大家来我的博客留言:https://sxq222.github.io/CSS%...博客主页:https://sxq222.github.io 正文 ...

  7. jquery 常用选择器和方法以及遍历(超详细)

    jQuery 常用选择器和和方法 学习总结 一.JQuery 介绍 1. 什么是 jQuery 2. jQuery 版本介绍 3. jQuery 文件介绍 二.jQuery 对象 1. jQuery ...

  8. Idea导出jar包和使用自定义API

    自定义jar简单实现案例 学习内容 1. 自定义工具类 2. 导出jar 3. 加载Jar包 4. 调用自定义的API方法 总结 学习内容 1. 自定义工具类 新建一个java项目,然后创建包和工具类 ...

  9. phpstorm配置xdebug 3.0最新教程!!!配置不成功的快看!

    前言 之前2月份就开始配置xdebug,始终没有成功. 今天看到一篇写得挺详细的文章,心血来潮又折腾了下,可惜没成功. 验证始终说我配置错误 后面去阅读官方的文档,修改了些配置,居然搞成功了!! ni ...

  10. Python入门-内置对象函数

    1.callable() 查看函数知否可调用,可调用返回True,不可用返回False print("input函数:", callable(input)) #input函数: T ...