查看原文
其他

Gradle 系列 (五)、自定义 Gradle Transform

JsonChao 2023-01-12

Editor's Note

如何从零到一自定义 Gradle Transform?推荐阅读~

The following article is from sweetying Author sweetying

这是 JsonChao 的第 290 期分享

前言

关于 Gradle 学习,我所理解的流程如下图:

在本系列的前 4 篇文章中,我们了解了:

1、Groovy 语法

2、Gradle 常用 api,生命周期及 hook 点,Task 定义,依赖与挂接到构建流程的基本操作

3、自定义 Gradle 插件及实战演练

还不清楚上面这些知识点的朋友,建议先去阅读我创建的Gradle 学习专栏

下面我抛出一些问题,大家可以思考下🤔:

1、为了对 app 性能做一个全面的评估,我们需要做 UI,内存,网络等方面的性能监控,如何做?

2、发现某个第三方库有 bug ,用起来不爽,但又不想拿它的源码修改在重新编译,有什么好的办法?

3、我想在不修改源码的情况下,统计某个方法的耗时,对某个方法做埋点,怎么做?

为了实现上面的想法,可能我们最开始的第一反应:是能否通过 APT,反射,动态代理来实现?但是想来想去,这些方案都不能很好的满足上面的需求,而且,有些问题不能从 Java 源文件入手,我们应该从 Class 字节码文件寻找突破口

JVM 平台上,修改、生成字节码无处不在,从 ORM 框架(如 Hibernate, MyBatis)到 Mock 框架(如 Mockito),再到 Java Web 中的常⻘树 Spring 家族,再到新兴的 JVM 语言 Kotlin 编译器,还有大名鼎鼎的 cglib,都有字节码的身影

字节码相关技术的强大之处自然不用多说,而在 Android 开发中,无论是使用 Java 开发还是 Kotlin 开发,都是 JVM 平台的语言,所以如果我们在 Android 开发中,使用字节码技术做一下 hack,还可以天然地兼容 Java 和 Kotlin 语言

现在目的很明确,我们就是要通过修改字节码的技术去解决上面的问题,那这和我们今天要讲的 Gradle Transform 有什么关系呢?

接下来我们就进入 Gradle Transform 的学习

一、Gradle Transform 介绍

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

一图胜千言:

虽然在 AGP 7.0 中 Transform 被标记为废弃了,但还可以使用,并不妨碍我们的学习,但是会在 AGP 8.0 中移除。

后续文章我也会讲如何适配使用新的 Api 去进行 Transform 的替换,因此大家不用担心🍺

二、自定义 Gradle Transform

先不管细节,咱们直接实现一个自定义 Gradle Transform 在说,按照下面的步骤,保姆式教程

实现一个 Transform 需要先创建 Gradle 插件,大致流程:自定义 Gradle 插件 -> 自定义 Transform -> 注册 Transform

如果你了解自定义 Gradle 插件,那么自定义 Gradle Transform 将会变得非常简单,不了解的去看我的这篇文章Gradle 系列 (三)、Gradle 插件开发

首先给大家看一眼我项目初始化的一个配置:

可以看到:

1、AGP 版本:7.2.0

2、Gradle 版本:7.4

我的 AndroidStudio 版本:Dolphin | 2021.3.1

大家需要对应好 AndroidStudio 版本所需的 AGP 版本,AGP 版本所需的 Gradle 版本,否则会出现兼容性和各种未知的问题,对应关系可以去官网查询

另外大家会发现,AGP 7.x 中 settings.gradle 和根 build.gradle 文件使用了一种新的配置方式,建议改回原来的配置方式,坑少😄:

//1、修改 settings.gradle 
rootProject.name = "GradleTransformDemo"
include ':app'

//2、修改根 build.gradle
buildscript {
    ext.kotlin_version = "1.7.20"
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        classpath "com.android.tools.build:gradle:7.2.0"
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.7.20"
    }
}

allprojects {
    repositories {
        google()
        mavenCentral()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

2.1、自定义 Gradle 插件

创建 Gradle 插件 Module:customtransformplugin,初始代码如下图:

注意:此插件我是使用 Kotlin 编写的,和之前 Groovy 编写插件的区别:

1、Kotlin 编写的插件可以直接写在 src/main/java目录下,另外 AndroidStudio 对 Kotlin 多了很多扩展支持,编写效率高

2、 Groovy 编写插件需要写在src/main/groovy目录下

Transform 相关 Api 需要如下依赖:

implementation "com.android.tools.build:gradle-api:7.2.0"

但是上述并没有引入,是因为 AGP 相关 Api 依赖了它,根据依赖传递的特性,因此我们可以引用到 Transform 相关 Api

2.2、自定义 Transform

初始代码如下图:

接着对其进行简单的修改:

package com.dream.customtransformplugin

import com.android.build.api.transform.*
import com.android.build.gradle.internal.pipeline.TransformManager
import com.android.utils.FileUtils

/**
 * function: 自定义 Transform
 */

class MyCustomTransform: Transform() {

    /**
     * 设置我们自定义 Transform 对应的 Task 名称,Gradle 在编译的时候,会将这个名称经过一些拼接显示在控制台上
     */

    override fun getName(): String {
        return "ErdaiTransform"
    }

    /**
     * 项目中会有各种各样格式的文件,该方法可以设置 Transform 接收的文件类型
     * 具体取值范围:
     * CONTENT_CLASS:Java 字节码文件,
     * CONTENT_JARS:jar 包
     * CONTENT_RESOURCES:资源,包含 java 文件
     * CONTENT_DEX:dex 文件
     * CONTENT_DEX_WITH_RESOURCES:包含资源的 dex 文件
     *
     * 我们能用的就两种:CONTENT_CLASS 和 CONTENT_JARS
     * 其余几种仅 AGP 可用
     */

    override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
        return TransformManager.CONTENT_CLASS
    }

    /**
     * 定义 Transform 检索的范围:
     * PROJECT:只检索项目内容
     * SUB_PROJECTS:只检索子项目内容
     * EXTERNAL_LIBRARIES:只检索外部库,包括当前模块和子模块本地依赖和远程依赖的 JAR/AAR
     * TESTED_CODE:由当前变体所测试的代码(包括依赖项)
     * PROVIDED_ONLY:本地依赖和远程依赖的 JAR/AAR(provided-only)
     */

    override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    /**
     * 表示当前 Transform 是否支持增量编译 true:支持 false:不支持
     */

    override fun isIncremental()Boolean {
        return false
    }

    /**
     * 进行具体的检索操作
     */

    override fun transform(transformInvocation: TransformInvocation?) {
        printLog()
        transformInvocation?.inputs?.forEach {
            // 一、输入源为文件夹类型
            it.directoryInputs.forEach {directoryInput->
                //1、TODO 针对文件夹进行字节码操作,这个地方我们就可以做一些狸猫换太子,偷天换日的事情了
                //先对字节码进行修改,在复制给 dest
   
                //2、构建输出路径 dest
                val dest = transformInvocation.outputProvider.getContentLocation(
                    directoryInput.name,
                    directoryInput.contentTypes,
                    directoryInput.scopes,
                    Format.DIRECTORY
                )
                //3、将文件夹复制给 dest ,dest 将会传递给下一个 Transform
                FileUtils.copyDirectory(directoryInput.file,dest)
            }

            // 二、输入源为 jar 包类型
            it.jarInputs.forEach { jarInput->
                //1、TODO 针对 jar 包进行相关处理

                //2、构建输出路径 dest
                val dest = transformInvocation.outputProvider.getContentLocation(
                    jarInput.name,
                    jarInput.contentTypes,
                    jarInput.scopes,
                    Format.JAR
                )
                //3、将 jar 包复制给 dest,dest 将会传递给下一个 Transform 
                FileUtils.copyFile(jarInput.file,dest)
            }
        }
    }

    /**
     * 打印一段 log 日志
     */

    fun printLog() {
        println()
        println("******************************************************************************")
        println("******                                                                  ******")
        println("******                欢迎使用 ErdaiTransform 编译插件                    ******")
        println("******                                                                  ******")
        println("******************************************************************************")
        println()
    }

}

2.3、注册 Transform

在 CustomTransformPlugin 中对 TransForm 进行注册,如下:

/**
 * 自定义:CustomTransformPlugin
 */

class CustomTransformPlugin: Plugin<Project> {

    override fun apply(project: Project) {
        println("Hello CustomTransformPlugin")

        //新增的代码
        // 1、获取 Android 扩展
        val androidExtension = project.extensions.getByType(AppExtension::class.java)
        // 2、注册 Transform
        androidExtension.registerTransform(MyCustomTransform())
    }
}

ok,经过上面三步,一个最简单的自定义 Gradle Transform 插件已经完成了

2.4、上传插件到本地仓库

点击 publish进行发布

如果你的项目多了如下内容,则证明发布成功了

2.5、效果验证

在根 build.gradle 进行插件依赖:

buildscript {
    //...
    repositories {
       //...
       //添加本地 maven 仓库
        maven {
            url('repo')
        }
    }
    dependencies {
        //...
       //引入插件依赖
        classpath "com.dream:customtransformplugin:1.0.0"
    }
}

在 app 的 build.gradle 应用插件:

plugins {
    //...
   //应用插件
    id 'CustomTransformPlugin'
}

同步一下项目,运行 app

配置阶段打印如下图:

执行阶段打印如下图:

这样我们一个最简单的自定义 Gradle Transform 就完成了

另外需要注意:当你对自定义 Gradle Transform 做修改后想看效果,务必升级插件的版本,重新发布,然后在根 build.gradle 中修改为新的版本,同步后在重新运行,否则 Gradle Transform 会不生效

消化一下,接下来我们讲点 Transform 的细节

三、Transform 细节和相关 Api 介绍

3.1、Transform 数据流动

Transform 数据流动主要分为两种:

1、消费型 Transform :数据会输出给下一个 Transform

2、引用型 Transform :数据不会输出给下一个 Transform

3.1.1、消费型 Transform

如下图:

1、每个 Transform 其实都是一个 Gradle Task,AGP 中的 TaskManager 会将每个 Transform 串连起来

2、第一个 Transform 会接收:

1、来自 Javac 编译的结果

2、拉取到在本地的第三方依赖(jar,aar)

3、resource 资源(这里的 resource 并非 Android 项目中的 res 资源,而是 assets 目录下的资源)

3、这些编译的中间产物,会在 Transform 组成的链条上流动,每个 Transform 节点可以对 Class 进行处理再传递给下一个Transform

4、我们常⻅的混淆,Desugar 等逻辑,它们的实现都是封装在一个个 Transform 中,而我们自定义的 Transform,会插入到这个Transform 链条的最前面

3.1.2、引用型 Transform

引用型 Transform 会读取上一个 Transform 输入的数据,而不需要输出给下一个Transform,例如 Instant Run 就是通过这种方式,检查两次编译之间的 diff 进行快速运行

ok,了解了 Transform 的数据流动,我们回到自定义 Transform 的初始状态,如下:

class MyCustomTransform: Transform() {

    override fun getName(): String {
        return "ErdaiTransform"
    }

    override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
        return TransformManager.CONTENT_CLASS
    }

    override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    override fun isIncremental()Boolean {
        return false
    }

    override fun transform(transformInvocation: TransformInvocation?) {
       super.transform(transformInvocation)
    }
}

我们重写了 Transform 的 5 个方法,接下来具体介绍下

3.2、getName

override fun getName(): String {
    return "ErdaiTransform"
}

getName 方法主要是获取自定义 Transform 的名称,可以看到它接收的是一个 String 字符串的类型,它的作用:

1、进行 Transform 唯一标识,一个应用内可以有多个 Transform,因此需要一个名称,方便后面调用

2、创建 Transform Task 命名时会用到它

通过源码验证一下,如下代码:

//TransformManager#addTransform
@NonNull
public <T extends Transform> Optional<TaskProvider<TransformTask>> addTransform(
            @NonNull TaskFactory taskFactory,
            @NonNull VariantCreationConfig creationConfig,
            @NonNull T transform,
            @Nullable PreConfigAction preConfigAction,
            @Nullable TaskConfigAction<TransformTask> configAction,
            @Nullable TaskProviderCallback<TransformTask> providerCallback) {
    //...
    List<TransformStream> inputStreams = Lists.newArrayList();
    String taskName = creationConfig.computeTaskName(getTaskNamePrefix(transform), "");
    //...
}

//TransformManager#getTaskNamePrefix
@VisibleForTesting
@NonNull
static String getTaskNamePrefix(@NonNull 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();   
}

注意:方法前后省略了大量代码,我们只看主线流程

从上面代码,我们可以看到新建的 Transform Task 的命名规则可以理解为:

transform${inputType1.name}And${inputType2.name}With${transform.name}For${variantName}

通过我们上面生成的 Transform Task 也可以验证这一点:

> Task :app:transformClassesWithErdaiTransformForDebug

3.3、getInputTypes

override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
    return TransformManager.CONTENT_CLASS
}

getInputTypes 方法主要用于获取输入类型,可以看到它接收一个 ContentType 的 Set 集合,表示它允许输入多种类型。上述返回值我们使用了 TransformManager 内置的输入类型,我们也可以自定义,如下:

override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
    //实际 TransformManager.CONTENT_CLASS 内部就是对它的封装
    return ImmutableSet.of(QualifiedContent.DefaultContentType.CLASSES)
}

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

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


// 加强类型,自定义 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),
    ;
  
    //...
}

自定义 Transform 我们可以在两个位置定义 ContentType:

1、Set getInputTypes(): 指定输入内容类型,允许通过 Set 集合设置输入多种类型

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

看一眼 TransformManager 给我们内置的 ContentType 集合,常用的是 CONTENT_CLASS :

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);
public static final Set<ContentType> CONTENT_DEX = ImmutableSet.of(ExtendedContentType.DEX);
public static final Set<ContentType> CONTENT_DEX_WITH_RESOURCES =
            ImmutableSet.of(ExtendedContentType.DEX, RESOURCES);

3.4、getScopes

override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
    return TransformManager.SCOPE_FULL_PROJECT
}

getScopes 方法主要用来定义检索的范围,告诉 Transform 需要处理哪些输入文件,可以看到它接收的是一个 Scope 的 Set 集合。上述返回值我们使用了 TransformManager 内置的 Scope 集合,如果不满足你的需求,你可以自定义,如下:

override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
    //实际 TransformManager.SCOPE_FULL_PROJECT 就是对它的封装
    return ImmutableSet.of(QualifiedContent.Scope.PROJECT, 
                    QualifiedContent.Scope.SUB_PROJECTS, 
                    QualifiedContent.Scope.EXTERNAL_LIBRARIES)
}

Scope 是一个枚举类:

enum Scope implements ScopeType {
    //只检索项目内容
    PROJECT(0x01),
    //只检索子项目内容
    SUB_PROJECTS(0x04),
    //只检索外部库,包括当前模块和子模块本地依赖和远程依赖的 JAR/AAR
    EXTERNAL_LIBRARIES(0x10),
    //由当前变体所测试的代码(包括依赖项)
    TESTED_CODE(0x20),
    //本地依赖和远程依赖的 JAR/AAR(provided-only)
    PROVIDED_ONLY(0x40),
}

自定义 Transform 可以在两个位置定义 Scope:

1、Set getScopes() 消费型输入内容范畴: 此范围的内容会被消费,因此当前 Transform 必须将修改后的内容复制到 Transform 的中间目录中,否则无法将内容传递到下一个 Transform 处理

2、Set getReferencedScopes() 指定引用型输入内容范畴: 默认是空集合,此范围的内容不会被消费,因此不需要复制传递到下一个 Transform,也不允许修改。

看一眼 TransformManager 给我们内置的 Scope 集合,常用的是 SCOPE_FULL_PROJECT 。需要注意,Library 模块注册的 Transform 只能使用 Scope.PROJECT

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);

3.5、isIncremental

override fun isIncremental()Boolean {
    return false
}

isIncremental 方法主要用于获取是否是增量编译,true:是, false:否。一个自定义 Transform 应该尽可能支持增量编译,这样可以节省一些编译的时间和资源,这个我们一会单独讲

3.6、transform

override fun transform(transformInvocation: TransformInvocation?) {
    super.transform(transformInvocation)
}

transform 方法主要用于对输入的数据做检索操作,它是 Transform 的核心方法,方法的参数是 TransformInvocation,它是一个接口,提供了所有与输入输出相关的信息:

public interface TransformInvocation {
  
    //...

    // 消费型输入内容
    Collection<TransformInput> getInputs();

    // 引用型输入内容
    Collection<TransformInput> getReferencedInputs();
  
    //...

    // 输出信息
    TransformOutputProvider getOutputProvider();

    // 是否增量构建
    boolean isIncremental();
}

1、isIncremental(): 当前 Transform 任务是否增量构建;

2、getInputs(): 获取 TransformInput 对象,它是消费型输入内容,对应于 Transform#getScopes() 定义的范围;

3、getReferencedInputs(): 获取 TransformInput 对象,它是引用型输入内容,对应于 Transform#getReferenceScope() 定义的内容范围;

4、getOutPutProvider(): TransformOutputProvider 是对输出文件的抽象。

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

1、DirectoryInput 集合: 以源码方式参与构建的输入文件,包括完整的源码目录结构及其中的源码文件;

2、JarInput 集合: 以 jar 和 aar 依赖方式参与构建的输入文件,包含本地依赖和远程依赖。

输出内容 TransformOutputProvider 有两个主要功能:

1、deleteAll(): 当 Transform 运行在非增量构建模式时,需要删除上一次构建产生的所有中间文件,可以直接调用 deleteAll() 完成;

2、getContentLocation(): 获得指定范围+类型的输出目标路径。

四、Transform 的增量与并发

到此为止,看起来 Transform 用起来也不难,但是,如果直接这样使用,会大大拖慢编译时间,为了解决这个问题,摸索了一段时间,也借鉴了Android 编译器中 Desugar 等几个 Transform 的实现,发现我们可以使用增量编译,并且上面 transform 方法遍历处理每个jar/class 的流程,其实可以并发处理,加上一般编译流程都是在 PC 上,所以我们可以尽量敲诈机器的资源。

上面也讲了,想要开启增量编译,只需要重写 Transform 的这个方法,返回 true 即可:

override fun isIncremental()Boolean {
    //开启增量编译
    return true
}

嗯,没了,已经开启了😄。有这么简单就好了,言归正传:

1、如果不是增量编译,则会清空 output 目录,然后按照前面的方式,逐个处理 class/jar 。

2、如果是增量编译,则会检查每个文件的 Status,Status 分四种:

public enum Status {

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

根据不同的 Status 处理逻辑即可

3、实现增量编译后,我们最好也支持并发编译,并发编译的实现并不复杂,原理:对上面处理单个 class/jar 的逻辑进行并发处理,最后阻塞等待所有任务结束即可

4.1、自定义 Tranform 模版

整个 Transform 的核心过程是有固定套路的,模板流程引入诗与远方的一张图:

接下来,我们就按照上面这张图,来处理 Transform 的增量和并发,并封装一套通用的模版代码,下面模版写了详细的注释:

注意:WaitableExecutor 在 AGP 7.0 中已经引用不到了,因此我们需要手动添加WaitableExecutor源码

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

    //线程池,可提升 80% 的执行速度
    private var waitableExecutor: WaitableExecutor = WaitableExecutor.useGlobalSharedThreadPool()

    /**
     * 此方法提供给上层进行字节码插桩
     */

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

    /**
     * 上层可重写该方法进行文件过滤
     */

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


    /**
     * 默认:获取输入的字节码文件
     */

    override fun getInputTypes(): MutableSet<QualifiedContent.ContentType> {
        return TransformManager.CONTENT_CLASS
    }

    /**
     * 默认:检索整个项目的内容
     */

    override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
        return TransformManager.SCOPE_FULL_PROJECT
    }


    /**
     * 默认开启增量编译
     */

    override fun isIncremental()Boolean {
        return true
    }

    /**
     * 对输入的数据做检索操作:
     * 1、处理增量编译
     * 2、处理并发逻辑
     */

    override fun transform(transformInvocation: TransformInvocation) {
        super.transform(transformInvocation)

        log("Transform start...")

        //输入内容
        val inputProvider = transformInvocation.inputs
        //输出内容
        val outputProvider = transformInvocation.outputProvider

        // 1. 子类实现字节码插桩操作
        val function = provideFunction()

        // 2. 不是增量编译,删除所有旧的输出内容
        if (!transformInvocation.isIncremental) {
            outputProvider.deleteAll()
        }

        for (input in inputProvider) {
            // 3. Jar 包处理
            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. 增量编译中处理 Jar 包逻辑
                    when (jarInput.status ?: Status.NOTCHANGED) {
                        Status.NOTCHANGED -> {
                            // Do nothing.
                        }
                        Status.ADDED, Status.CHANGED -> {
                            // Do transform.
                            waitableExecutor.execute {
                                doTransformJar(inputJar, outputJar, function)
                            }
                        }
                        Status.REMOVED -> {
                            // Delete.
                            FileUtils.delete(outputJar)
                        }
                    }
                } else {
                    // 3.2 非增量编译中处理 Jar 包逻辑
                    waitableExecutor.execute {
                        doTransformJar(inputJar, outputJar, function)
                    }
                }
            }
            // 4. 文件夹处理
            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. 增量编译中处理文件夹逻辑
                    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.
                                waitableExecutor.execute {
                                    doTransformFile(inputFile, outputFile, function)
                                }
                            }
                            Status.REMOVED -> {
                                // Delete
                                FileUtils.delete(outputFile)
                            }
                        }
                    }
                } else {
                    // 4.2. 非增量编译中处理文件夹逻辑
                    // Traversal fileTree (depthFirstPreOrder).
                    for (inputFile in FileUtils.getAllFiles(inputDir)) {
                        waitableExecutor.execute {
                            val outputFile = concatOutputFilePath(outputDir, inputFile)
                            if (classFilter(inputFile.name)) {
                                doTransformFile(inputFile, outputFile, function)
                            } else {
                                // Copy.
                                Files.createParentDirs(outputFile)
                                FileUtils.copyFile(inputFile, outputFile)
                            }
                        }
                    }
                }
            }
        }
        waitableExecutor.waitForTasksWithQuickFail<Any>(true)
        log("Transform end...")
    }

    /**
     * Do transform Jar.
     */

    private fun doTransformJar(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) {
                                zos.putNextEntry(ZipEntry(entry.name))
                                if (classFilter(entry.name)) {
                                    // Apply transform function.
                                    applyFunction(zis, zos, function)
                                } else {
                                    // Copy.
                                    zis.copyTo(zos)
                                }
                            }
                            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 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 concatOutputFilePath(outputDir: File, inputFile: File) = File(outputDir, inputFile.name)

    /**
     * log 打印
     */

    private fun log(logStr: String) {
        if (enableLog) {
            println("$name - $logStr")
        }
    }
}

上述模版给我们做了大量工作: Trasform 的输入文件遍历、加解压、增量,并发等,我们只需要专注字节码文件的修改即可

五、自定义模版使用

ok,接下来修改自定义 Gradle Transform 的代码:

package com.dream.customtransformplugin

import java.io.InputStream
import java.io.OutputStream

/**
 * function: 自定义 Transform
 */

class MyCustomTransform: BaseCustomTransform(true) {

    override fun getName(): String {
        return "ErdaiTransform"
    }

    /**
     * 此方法可以使用 ASM 或 Javassist 进行字节码插桩
     * 目前只是一个默认实现
     */

    override fun provideFunction() = { ios: InputStream, zos: OutputStream ->                          
        zos.write(ios.readAllBytes())
    }
}

是不是瞬间清爽了很多,发布一个新的插件版本,修改根 build.gradle 插件的版本,同步后重新运行 app,效果如下:

六、总结

本篇文章我们主要介绍了:

1、Gradle Transform 是什么?

简单的理解:我们可以自定义 Gradle Transform 修改字节码文件实现编译插桩

2、使用 Kotlin 编写自定义 Gradle Transform 的流程,注意和 Groovy 编写插件的区别

1、Kotlin 编写插件可直接写在 src/main/java 目录下

2、Groovy 编写插件需写在 src/main/groovy 目录下

3、介绍了 Transform 的数据流动和自定义 Gradle Transform 实现的相关 Api

4、介绍了 Transform 的增量与并发,并封装了一个模版,简化我们自定义 Gradle Transform 的使用

另外,本篇文章,我们只是讲了 Gradle Transform 简单使用,还没有做具体的插桩逻辑,因此前言中的问题暂时还解决不了

预知后事如何,请听下回分解

好了,本篇文章到这里就结束了,希望能给你带来帮助 🤝

Github Demo 地址:https://github.com/sweetying520/GradleTransformDemo , 大家可以结合 demo 一起看,效果杠杠滴🍺


END




往期推荐



一文深入了解 HashMap 设计思想

Android 增量更新完全解析 是增量不是热修复

沪江学习 Android 端重构实践

贝塞尔Loading——化学风暴

从原理到实战,全面总结 Android HTTPS 抓包


点击下方卡片关注 JsonChao,为你构建一套

大厂青睐的 T 型人才系统


▲ 点击上方卡片关注 JsonChao,构建一套

大厂青睐的 T 型人才知识体系

欢迎把文章分享到朋友圈


大厂 T 型人才成长社群

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存