其实 Gradle Transform 就是个纸老虎 —— Gradle 系列(4)
前言
目前,使用 AGP Transform API 进行字节码插桩已经非常普遍了,例如 Booster、神策等框架中都有 Transform 的影子。Transform 听起来很高大上,其本质就是一个 Gradle Task。在这篇文章里,我将带你理解 Transform 的工作机制、使用方法和核心源码解析,并通过一个 Demo 帮助你融会贯通。
这篇文章是全面掌握 Gradle 构建系统系列的第 4 篇:
- 1、Gradle 基础
- 2、Gradle 插件
- 3、Gradle 依赖管理
- 4、APG Transform
请点赞加关注,你的支持对我非常重要,满足下我的虚荣心。
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 中定义了 DefaultContentType 和 ExtendedContentType 两个枚举类。但是,我们在自定义 Transform 时只能使用 DefaultContentType 中定义的枚举,即 CLASSES 和 RESOURCES 两种类型,其它类型仅供 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 中定义了 InternalScope 和 Scope 两个枚举类。但是,我们在自定义 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 + ASM 探索 —— REBOOTERS 著
- 深入理解 Transform —— toothpickTina 著
- 现在准备好告别 Transform 了吗? —— 究极逮虾户 著
- AGP Transform API 被废弃意味着什么? —— johnsonlee 著
- Transforming dependency artifacts on resolution —— Gradle 官方文档
你的点赞对我意义重大!微信搜索公众号 [彭旭锐],希望大家可以一起讨论技术,找到志同道合的朋友,我们下次见!
其实 Gradle Transform 就是个纸老虎 —— Gradle 系列(4)的更多相关文章
- 第二篇:gradle脚本运行环境分析(gradle的语义模型)
		引言:通过上一篇的论述,我们知道gradle脚本是如假包换的groovy代码,但是这个groovy代码是运行在他的上下文环境里面的,学名叫语义模型.这一篇我们就来看看他的语义模型到底是什么,如何使用. ... 
- [Gradle] 在 Eclipse 下利用 gradle 构建系统
		转载自:http://www.ibm.com/developerworks/cn/opensource/os-cn-gradle/ 构建系统时候常常要用到 Ant, Maven 等工具,对于初学者 ... 
- gradle入门(1-1)gradle的概念和使用
		一.Gradle是什么 Gradle是一种Java应用构建工具,它采用领域特定语言 Groovy 语法实现配置. 1.Gradle的基本概念 项目:项目的配置 即 build.gradle. 任务:任 ... 
- Gradle安装   Gradle效率提升  eclipse安装gradle插件 【我】
		Gradle安装 从官网下载 gradle4.6版本,也可以从svn地址下载 https://downloads.gradle.org/distributions/gradle-4.6-bin.zip ... 
- Eclipse集成Gradle 【Eclipse在线安装Gradle插件方法】
		本章将介绍了Eclipse集成Gradle.以下是将Gradle插件添加到Eclipse的步骤. 步骤1 - 打开Eclipse Marketplace 打开在系统中安装好的Eclipse. 转到 J ... 
- 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 ... 
- Gradle修改缓存路径 和 Gradle修改Maven仓库地址
		Gradle修改缓存路径 修改gradle启动脚本进入gradle安装的bin目录,使用文本编辑器打开gradle.bat文件,在如图的位置添加以下语句 set GRADLE_OPTS="- ... 
- 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 ... 
- 创建Gradle工程出现Could not install Gradle distribution from 'https://services.gradle.org/distributions/gradleXX'.问题解决
		在 Eclipse EE Oxygen 中创建 Gradle Project的时候 出现如下错误: org.gradle.tooling.GradleConnectionException: Coul ... 
随机推荐
- C语言之关键字(知识点2)
			关键字又叫保留字,这些关键字不可以再次定义 解析 
- 设置python  虚拟环境  virtualenv     django 虚拟环境
			https://developer.mozilla.org/en-US/docs/Learn/Server-side/Django/development_environment Ubuntu vir ... 
- CSS中宽度与高度
			div的高度 div由的高度是由它里面的字体乘以字体建议的行高确定,跟这个字体大小没有关系(不同字体相同字体大小,会影响div的高度.)如过明确告诉浏览器行高,div高度就是行高. 文字两端对齐: 文 ... 
- 行内元素的padding和margin是否有效
			行内元素的纵向padding和margin都是不考虑的,这是css规范定义的.inline元素确实可以设置垂直方向的padding和margin值,但是inline元素的margin和padding的 ... 
- 前端规范(ES6BEMOOCSSSMACSS)
			前端规范 在实际开发中,由于团队成员编码习惯不一,技术层次不同,开发前定制并遵循一种代码规范能提高代码质量,增加开发效率. Javascript Javascript规范直接参考airbnb: ES6 ... 
- 如何用vue打造一个移动端音乐播放器
			写在前面 没错,这就是慕课网上的那个vue音乐播放器,后台是某音乐播放器的线上接口扒取,虽然这类项目写的人很多,但不得不说这还是个少有的适合vue提升的好项目,做这个项目除了想写一个比较大并且功能复杂 ... 
- java中抛出throw关键字是怎么用的? 举例?
			5.抛出throw关键字 马克-to-win:我们先说5/0的原理,当程序运行到5/0的时候,java系统JVM会在后台new出一个除0异常实例,之后把这个实例传入catch块儿供开发者使用.马克-t ... 
- 关于Symbol.iterator 学习笔记
			1.可以部署在对象上的一个遍历器 2. 遍历器是一个函数,需要返回一个含有一个next 方法的对象 const likeArray = {0:'a', 1: 'b', 2: 'c',3: 'd'. l ... 
- Vue入坑日记: day - 01
			前言 最近做了一些小项目,小组里写前端的确实有点拉胯,于是自己动手写前端,因为大一学过web前端基础,所以对HTML,CSS还有印象,就直接对JS下手了,学了两天把JS大致搞明白了,顺便对JQuery ... 
- Spring配置文件-Bean生命周期配置(init-method方法,destory-method方法)
			1.UserDaoImpl类 public class UserDaoImpl implements UserDao { public UserDaoImpl(){ System.out.printl ... 
