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

继续创作,加速成长!这是我参与「日新计划 · 10 月更文挑战」的第5天,点击查看活动详情

前言

很高兴遇见你~

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

Gradle_learning

在本系列的前 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 等字节码修正工具进行修正,插入自界说逻辑

一图胜千言:

transfrom.webp

虽然在 AGP 7.0 中 Transform 被标记为废弃了,但还能够运用,并不阻碍咱们的学习,可是会在 AGP 8.0 中移除。

后续文章我也会讲怎么适配运用新的 Api 去进行 Transform 的替换,因而咱们不必担心🍺

二、自界说 Gradle Transform

先不论细节,咱们直接完结一个自界说 Gradle Transform 在说,依照下面的进程,保姆式教程

完结一个 Transform 需求先创立 Gradle 插件,大致流程:自界说 Gradle 插件 -> 自界说 Transform -> 注册 Transform

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

首先给咱们看一眼我项目初始化的一个装备:

image-20221027193356475.png

能够看到:

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,初始代码如下图:

image-20221028155752896.png

留意:此插件我是运用 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

初始代码如下图:

image-20221027225223971.png

接着对其进行简略的修正:

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进行发布

image-20221027231411456.png

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

image-20221027231609120.png

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

装备阶段打印如下图:

image-20221027232532681.png

履行阶段打印如下图:

image-20221027232433364.png

这样咱们一个最简略的自界说 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

如下图:

image-20221027234522100.png

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 的核心进程是有固定套路的,模板流程引进诗与远方的一张图:

transforms.png

接下来,咱们就依照上面这张图,来处理 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,作用如下:

image-20221029150353716.png

六、总结

本篇文章咱们首要介绍了:

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 地址 , 咱们能够结合 demo 一同看,作用杠杠滴🍺

感谢你阅览这篇文章

参阅和推荐

Gradle 系列(8)其实 Gradle Transform 便是个纸老虎

Gradle Transform + ASM 探索

Android Gradle 插件版别说明

你的点赞,评论,是对我巨大的鼓励!

欢迎重视我的大众号: sweetying ,文章更新可榜首时刻收到

假如有问题,大众号内有加我微信的进口,在技能学习、个人成长的道路上,咱们一同前进!