TransformAPI是什么

从 1.5.0-beta1 开始,Gradle 插件包含一个 Transform API,允许 3rd 方插件在将编译的类文件转换为 dex 文件之前对其进行操作。(API 在 1.4.0-beta2 中存在,但在 1.5.0-beta1 中已完全修改)
此 API 的目标是简化注入自定义类操作而无需处理任务,并为操作的内容提供更大的灵活性。内部代码处理(jacoco、progard、multi-dex)在 1.5.0-beta1 中已经全部转移到这个新机制。
注意:这仅适用于 javac/dx 代码路径。Jack 目前不使用此 API。

以上是Android官方对于TransformAPI的介绍,地址在这里

简单来说,TransformAPI可以让我们在编译打包安卓项目时,在源码编译为class字节码后,处理成dex文件前,对字节码做一些操作。

编写Transform

本博文通过编写一个TransformAPI实例来介绍如何在Android项目中使用TransformAPI,下面就跟着本文一起来实现吧:

  1. 使用Android Studio创建Android项目,这里我取名为TransformDemo

  2. 使用buildSrc的方式创建一个gradle插件,如果你对这种方式不了解的话,可以参考我的上一篇博文:Android Gradle插件开发基础 创建好的插件目录结构如下:
    在这里插入图片描述

  3. buildSrc目录下创建build.gradle文件,加入如下代码:

    apply plugin: 'groovy'
    apply plugin: 'maven'
    
    repositories {
        google()
        mavenCentral()
    }
    
    dependencies {
        implementation gradleApi()
        implementation localGroovy()
        implementation 'com.android.tools.build:gradle:4.2.2'
    }
    
    sourceSets {
        main {
            java {
                srcDir 'src/main/java'
            }
            resources {
                srcDir 'src/main/resources'
            }
        }
    }
    
    java {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    
  4. 在插件包com.test.plugin中创建一个MyPlugin类,代码如下:

    package com.test.plugin;
    
    import com.android.build.gradle.BaseExtension;
    
    import org.gradle.api.Plugin;
    import org.gradle.api.Project;
    
    public class MyPlugin implements Plugin<Project> {
    
        @Override
        public void apply(Project project) {
        	// 将Transform注册到插件中,执行插件时就会自动执行该Transform
            BaseExtension ext = project.getExtensions().findByType(BaseExtension.class);
            if (ext != null) {
                ext.registerTransform(new MyTransform());
            }
        }
    }
    
  5. 创建MyTransform类,代码如下,具体的代码解释放到后面:

    package com.test.plugin;
    
    import com.android.build.api.transform.QualifiedContent;
    import com.android.build.api.transform.Transform;
    import com.android.build.api.transform.TransformException;
    import com.android.build.api.transform.TransformInvocation;
    import com.android.build.gradle.internal.pipeline.TransformManager;
    
    import java.io.IOException;
    import java.util.Set;
    
    public class MyTransform extends Transform {
    
        @Override
        public String getName() {
            // 最终执行时的任务名称为transformClassesWithMyTestFor[XXX] (XXX为Debug或Release)
            return "MyTest";
        }
    
        @Override
        public Set<QualifiedContent.ContentType> getInputTypes() {
            return TransformManager.CONTENT_CLASS;
        }
    
        @Override
        public Set<? super QualifiedContent.Scope> getScopes() {
            return TransformManager.SCOPE_FULL_PROJECT;
        }
    
        @Override
        public boolean isIncremental() {
            return false;
        }
    
        @Override
        public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
            super.transform(transformInvocation);
            System.out.println("Hello MyTransform...");
        }
    }
    
    
  6. 记得在src/main/resources目录下注册插件,注册方法就不详细说了,上一篇里有详细记录:Android Gradle插件开发基础

  7. 在app module下的build.gradle文件中引用创建的插件,引用方法是直接在文件头加入该配置:apply plugin: 'com.test.plugin'

  8. 然后我们使用Gradle的sync工具同步一下,可以看到日志里输出如下:
    在这里插入图片描述
    上图中的Hello MyTransform...的日志就是我们写在MyTransform类的transform方法中打印的。

以上Demo的源码可以在这里下载:https://github.com/yubo725/android-transform-demo

TransformAPI详解

以上通过一个简单的例子展示了在Android项目中如何使用TransformAPI,然而你可能并不了解Transform的详细用法,并且也不知道TransformAPI的具体应用在什么地方,下面我整理了一份详细的TransformAPI的用法:

Transform是一个抽象类,其源码中对Transform做了详细的注释,翻译成中文表述如下:

处理中间构建工件的转换。
对于每个添加的转换,都会创建一个新任务。添加转换的操作负责处理任务之间的依赖关系。这是基于转换过程完成的。转换的输出可以被其他转换使用,并且这些任务会自动链接在一起。
转换表明它适用于什么(内容、范围)以及它生成什么(内容)。
转换接收输入作为集合 TransformInput,它由 JarInputs 和 DirectoryInputs 组成。两者都提供有关与其特定内容相关联的 QualifiedContent.Scopes 和 QualifiedContent.ContentTypes 的信息。
输出由 TransformOutputProvider 处理,它允许创建新的自包含内容,每个内容都与自己的范围和内容类型相关联。 TransformInput/Output 处理的内容由转换系统管理,它们的位置是不可配置的。
最佳做法是写入与转换接收到的 Jar/文件夹输入一样多的输出。将所有输入组合成单个输出可防止下游转换处理有限的范围。
虽然可以通过文件扩展名区分不同的内容类型,但对于 Scopes 则无法这样做。因此,如果转换请求一个范围,但唯一可用的输出包含的范围多于请求的范围,则构建将失败。
如果转换请求单个内容类型但唯一可用的内容包括比请求的类型更多的内容,则输入文件/文件夹将包含所有类型的所有文件,但转换应仅读取、处理和输出类型它要求。
此外,变换可以指示次要输入/输出。这些不由上游或下游转换处理,也不受由转换处理的类型的限制。他们可以是任何东西。
由每个转换来管理这些文件的位置,并确保在调用转换之前生成这些文件。这是通过注册转换时的附加参数来完成的。
这些辅助输入/输出允许转换读取但不处理任何内容。这可以通过让 getScopes() 返回一个空列表并使用 getReferencedScopes() 来指示要读取的内容来实现。

实现自定义的Transform一般要复写如下几个方法,下面对每个方法做一下详细解释:

getName()

getName()方法用于指明自定义的Transform的名称,在gradle执行该任务时,会将该Transform的名称再加上前后缀,如上面图中所示的,最后的task名称是transformClassesWithXXXForXXX这种格式。

getInputTypes()

用于指明 Transform 的输入类型,可以作为输入过滤的手段。在 TransformManager 类中定义了很多类型:

// 代表 javac 编译成的 class 文件,常用
public static final Set<ContentType> CONTENT_CLASS;
public static final Set<ContentType> CONTENT_JARS;
// 这里的 resources 单指 java 的资源
public static final Set<ContentType> CONTENT_RESOURCES;
public static final Set<ContentType> CONTENT_NATIVE_LIBS;
public static final Set<ContentType> CONTENT_DEX;
public static final Set<ContentType> CONTENT_DEX_WITH_RESOURCES;
public static final Set<ContentType> DATA_BINDING_BASE_CLASS_LOG_ARTIFACT;

getScopes()

用于指明 Transform 的作用域。同样,在 TransformManager 类中定义了几种范围:

// 注意,不同版本值不一样
public static final Set<Scope> EMPTY_SCOPES = ImmutableSet.of();
public static final Set<ScopeType> PROJECT_ONLY;
public static final Set<Scope> SCOPE_FULL_PROJECT; // 常用
public static final Set<ScopeType> SCOPE_FULL_WITH_IR_FOR_DEXING;
public static final Set<ScopeType> SCOPE_FULL_WITH_FEATURES;
public static final Set<ScopeType> SCOPE_FULL_WITH_IR_AND_FEATURES;
public static final Set<ScopeType> SCOPE_FEATURES;
public static final Set<ScopeType> SCOPE_FULL_LIBRARY_WITH_LOCAL_JARS;
public static final Set<ScopeType> SCOPE_IR_FOR_SLICING;

常用的是 SCOPE_FULL_PROJECT ,代表所有 Project 。

确定了 ContentType 和 Scope 后就确定了该自定义 Transform 需要处理的资源流。比如 CONTENT_CLASS 和 SCOPE_FULL_PROJECT 表示了所有项目中 java 编译成的 class 组成的资源流。

isIncremental()

指明该 Transform 是否支持增量编译。需要注意的是,即使返回了 true ,在某些情况下运行时,它还是会返回 false 的。

transform(TransformInvocation transformInvocation)

一般在这个方法中对字节码做一些处理。

TransformInvocation是一个接口,源码如下:

public interface TransformInvocation {

    /**
     * Returns the context in which the transform is run.
     * @return the context in which the transform is run.
     */
    @NonNull
    Context getContext();

    /**
     * Returns the inputs/outputs of the transform.
     * @return the inputs/outputs of the transform.
     */
    @NonNull
    Collection<TransformInput> getInputs();

    /**
     * Returns the referenced-only inputs which are not consumed by this transformation.
     * @return the referenced-only inputs.
     */
    @NonNull Collection<TransformInput> getReferencedInputs();
    /**
     * Returns the list of secondary file changes since last. Only secondary files that this
     * transform can handle incrementally will be part of this change set.
     * @return the list of changes impacting a {@link SecondaryInput}
     */
    @NonNull Collection<SecondaryInput> getSecondaryInputs();

    /**
     * Returns the output provider allowing to create content.
     * @return he output provider allowing to create content.
     */
    @Nullable
    TransformOutputProvider getOutputProvider();


    /**
     * Indicates whether the transform execution is incremental.
     * @return true for an incremental invocation, false otherwise.
     */
    boolean isIncremental();
}

我们既可以通过TransformInvocation来获取输入,同时也获得了输出的功能,举个例子:

public void transform(TransformInvocation invocation) {
    for (TransformInput input : invocation.getInputs()) {
        input.getJarInputs().parallelStream().forEach(jarInput -> {
        File src = jarInput.getFile();
        JarFile jarFile = new JarFile(file);
        Enumeration<JarEntry> entries = jarFile.entries();
        while (entries.hasMoreElements()) {
            JarEntry entry = entries.nextElement();
            //处理
        }
    }
}

后续文章中我会记录Gradle插件+TransformAPI+字节码插桩工具结合使用的相关应用。

参考

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐