一文学会Android Gradle Transform基础使用

2021-02-23 15:06:00 浏览数 (1)

概述

最近在做一个在 Android 工程编译期间动态插入一些随机代码的需求,我选择的是 Gradle Transform 技术,想起好久没有写过博客了,就记录一下这方面的一些基本使用。

一般来说,在 Android 工程的编译期间可以通过一些技术来动态插入一些代码逻辑甚至生成一些新的 Class 类,具体技术有:

  1. APT(Annotation Processing Tool): 编译期注解处理技术,通过自定义注解和注解处理器来实现编译期生成代码的功能,并且将生成的代码和源代码一起编译成 class 文件。
  2. AspectJ: 是一种编译器,在编译期间,将开发者编写的 Aspect 程序织入到目标程序中,扩展目标程序的功能。
  3. Transform&Javassist: Transform 是 Android Gradle 提供的操作字节码的一种方式。它在 class 编译成 dex 之前通过一系列 Transform 处理来实现代码注入。Javassist 可以方便地修改 .class 文件。

Android Gradle 工具从 1.5.0-beta1 版本开始提供了 Transform API 工具,它可以在将 .class 文件转换为 dex 文件之前对其进行操作。可以通过自定义 Gradle 插件来注册自定义的 Transform,注册后 Transform 会包装成一个 Gradle Task 任务,这个 Task 在 compile task 执行完毕后运行。

依赖如下:

代码语言:javascript复制
implementation 'com.android.tools.build:gradle:4.1.1'

当在buildSrc中开发插件时,其build.gradle脚本内容如下:

代码语言:javascript复制
apply plugin: 'groovy'
apply plugin: 'maven'

repositories {
    google()
    jcenter()
    mavenCentral()
}

dependencies {
    implementation gradleApi()
    implementation localGroovy()
    implementation 'com.android.tools.build:gradle:4.1.1'
}

Transform处理流程如下图(图片来于网络):

Transform

先看看Transform类,这是一个abstract类,实现自定义 Transform task 需要重写它,一般需要重写的方法有:

代码语言:javascript复制
class InjectTransform extends Transform {

    @Override
    String getName() {
        return null
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return null
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return null
    }

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        super.transform(transformInvocation)
    }
}

getName指明 Transform 的名字,也对应了该 Transform 所代表的 Task 名称,例如当返回值为 InjectTransform 时,编译后可以看到名为transformClassesWithInjectTransformForxxx 的 task。

getInputTypes指明 Transform 处理的输入类型,在 TransformManager 中定义了很多类型:

代码语言:javascript复制
public static final Set<ScopeType> EMPTY_SCOPES = ImmutableSet.of();

// 代表 javac 编译成的 class 文件,常用
public static final Set<ContentType> CONTENT_CLASS = ImmutableSet.of(CLASSES);
public static final Set<ContentType> CONTENT_JARS = ImmutableSet.of(CLASSES, RESOURCES);
// 这里的 resources 单指 java 的资源
public static final Set<ContentType> CONTENT_RESOURCES = ImmutableSet.of(RESOURCES);
public static final Set<ContentType> CONTENT_NATIVE_LIBS = ImmutableSet.of(NATIVE_LIBS);
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);

其中,很多类型是不允许自定义 Transform 来处理的,我们常使用 CONTENT_CLASS 来操作 Class 文件。

getScopes指明 Transform 输入文件所属的范围, 因为 gradle 是支持多工程编译的。总共有以下几种:

代码语言:javascript复制
enum Scope implements ScopeType {
    /** Only the project (module) content */
    PROJECT(0x01),
    /** Only the sub-projects (other modules) */
    SUB_PROJECTS(0x04),
    /** Only the external libraries */
    EXTERNAL_LIBRARIES(0x10),
    /** Code that is being tested by the current variant, including dependencies */
    TESTED_CODE(0x20),
    /** Local or remote dependencies that are provided-only */
    PROVIDED_ONLY(0x40),

    @Deprecated
    PROJECT_LOCAL_DEPS(0x02),
    @Deprecated
    SUB_PROJECTS_LOCAL_DEPS(0x08);

    private final int value;

    Scope(int value) {
        this.value = value;
    }

    @Override
    public int getValue() {
        return value;
    }
}

在 TransformManager 类中定义了几种范围:

代码语言:javascript复制
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);
public static final Set<ScopeType> SCOPE_FULL_WITH_FEATURES = new ImmutableSet.Builder<ScopeType>().addAll(SCOPE_FULL_PROJECT).add(InternalScope.FEATURES).build();
public static final Set<ScopeType> SCOPE_FEATURES = ImmutableSet.of(InternalScope.FEATURES);
public static final Set<ScopeType> SCOPE_FULL_LIBRARY_WITH_LOCAL_JARS = ImmutableSet.of(Scope.PROJECT, InternalScope.LOCAL_DEPS);
public static final Set<ScopeType> SCOPE_FULL_PROJECT_WITH_LOCAL_JARS = new ImmutableSet.Builder<ScopeType>().addAll(SCOPE_FULL_PROJECT).add(InternalScope.LOCAL_DEPS).build();

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

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

isIncremental指明该 Transform 是否支持增量编译。有时即使返回 true, 在某些情况下它还是会当作 false 返回。

transformtransform是一个空实现,input的内容将会打包成一个 TransformInvocation 对象。

TransformInvocation

看一下这个接口的定义:

代码语言:javascript复制
public interface TransformInvocation {

    // 上下文
    @NonNull
    Context getContext();

    // transform 的输入/输出
    @NonNull
    Collection<TransformInput> getInputs();

     // 返回不被这个 transformation 消费的 input
    @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.
     */
    @NonNull Collection<SecondaryInput> getSecondaryInputs();

    // 返回允许创建内容的 output provider
    @Nullable
    TransformOutputProvider getOutputProvider();

    boolean isIncremental();
}

TransformInput

代码语言:javascript复制
public interface TransformInput {
    // 表示 Jar 包
    @NonNull
    Collection<JarInput> getJarInputs();

    // 表示目录,包含 class 文件
    @NonNull
    Collection<DirectoryInput> getDirectoryInputs();
}

TransformOutputProvider

代码语言:javascript复制
public interface TransformOutputProvider {

    void deleteAll() throws IOException;

    // 根据 name、ContentType、QualifiedContent.Scope 返回对应的文件(jar / directory)
    @NonNull
    File getContentLocation(
            @NonNull String name,
            @NonNull Set<QualifiedContent.ContentType> types,
            @NonNull Set<? super QualifiedContent.Scope> scopes,
            @NonNull Format format);
}

示例:注入代码

首先创建一个普通的Android工程。

自定义Gradle插件,示例采用buildSrc方式。

新建 buildSrc 目录,其 build.gradle 内容如下:

代码语言:javascript复制
apply plugin: 'groovy'
apply plugin: 'maven'

repositories {
    google()
    jcenter()
    mavenCentral()
}

dependencies {
    implementation gradleApi()
    implementation localGroovy()
    implementation 'com.android.tools.build:gradle:4.1.1'
    implementation 'org.javassist:javassist:3.27.0-GA'
}

Transform代码:

代码语言:javascript复制
class InjectTransform extends Transform {

    private Project mProject

    InjectTransform(Project project) {
        this.mProject = project
    }

    @Override
    String getName() {
        return "InjectTransform"
    }

    @Override
    Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS
    }

    @Override
    Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT
    }

    @Override
    boolean isIncremental() {
        return false
    }

    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        transformInvocation.inputs.each { input ->
            // 包含我们手写的 Class 类及 R.class、BuildConfig.class 等
            input.directoryInputs.each { directoryInput ->
                String path = directoryInput.file.absolutePath
                println("[InjectTransform] Begin to inject: $path")

                // 执行注入逻辑
                InjectByJavassit.inject(path, mProject)

                // 获取输出目录
                def dest = transformInvocation.outputProvider.getContentLocation(directoryInput.name,
                        directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                println("[InjectTransform] Directory output dest: $dest.absolutePath")

                // 将input的目录复制到output指定目录
                FileUtils.copyDirectory(directoryInput.file, dest)
            }

            // jar文件,如第三方依赖
            input.jarInputs.each { jarInput ->
                def dest = transformInvocation.outputProvider.getContentLocation(jarInput.name,
                    jarInput.contentTypes, jarInput.scopes, Format.JAR)
                FileUtils.copyFile(jarInput.file, dest)
            }
        }
    }
}

Javassit代码:

代码语言:javascript复制
class InjectByJavassit {
    static void inject(String path, Project project) {
        try {
            File dir = new File(path)
            if (dir.isDirectory()) {
                dir.eachFileRecurse { File file ->
                    if (file.name.endsWith('Activity.class')) {
                        doInject(project, file, path)
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace()
        }
    }

    private static void doInject(Project project, File clsFile, String originPath) {
        println("[Inject] DoInject: $clsFile.absolutePath")
        String cls = new File(originPath).relativePath(clsFile).replace('/', '.')
        cls = cls.substring(0, cls.lastIndexOf('.class'))
        println("[Inject] Cls: $cls")

        ClassPool pool = ClassPool.getDefault()
        // 加入当前路径
        pool.appendClassPath(originPath)
        // project.android.bootClasspath 加入android.jar,不然找不到android相关的所有类
        pool.appendClassPath(project.android.bootClasspath[0].toString())
        // 引入android.os.Bundle包,因为onCreate方法参数有Bundle
        pool.importPackage('android.os.Bundle')

        CtClass ctClass = pool.getCtClass(cls)
        // 解冻
        if (ctClass.isFrozen()) {
            ctClass.defrost()
        }
        // 获取方法
        CtMethod ctMethod = ctClass.getDeclaredMethod('onCreate')

        String toastStr = 'android.widget.Toast.makeText(this, "I am the injected code", android.widget.Toast.LENGTH_SHORT).show();'

        // 方法尾插入
        ctMethod.insertAfter(toastStr)
        ctClass.writeFile(originPath)

        // 释放
        ctClass.detach()
    }
}

注册Transform:

代码语言:javascript复制
class TransformPlugin implements Plugin<Project> {

    @Override
    void apply(Project target) {
        target.android.registerTransform(new InjectTransform(target))
    }
}

引用插件。

代码语言:javascript复制
apply plugin: com.hearing.plugin.TransformPlugin

在工程模块中引入插件后,在编译时可以看到相关日志,查看相关 class 文件,可以看到插入后的代码。

文中内容如有错误欢迎指出,共同进步!觉得不错的留个赞再走哈~

0 人点赞