阅读 1952

一文学会Android Gradle Transform基础使用

概述

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

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

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

这里还可以看看 AOP 和 IOC 的一些概念,参考 AOP-IOC概述。在使用 Transform 之前需要先了解一下 Gradle 自定义插件的方式,可以选择最简单的方式实现,参考 自定义gradle插件

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

依赖如下:

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

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

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

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

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 中定义了很多类型:

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 是支持多工程编译的。总共有以下几种:

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 类中定义了几种范围:

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 返回。

transform

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

TransformInvocation

看一下这个接口的定义:

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

public interface TransformInput {
    // 表示 Jar 包
    @NonNull
    Collection<JarInput> getJarInputs();

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

TransformOutputProvider

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);
}
复制代码

示例:注入代码

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

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

关于自定义 Gradle 插件的三种方式可以参考 自定义gradle插件

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

    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代码:

    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代码:

    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:

    class TransformPlugin implements Plugin<Project> {
    
        @Override
        void apply(Project target) {
            target.android.registerTransform(new InjectTransform(target))
        }
    }
    复制代码

3.引用插件。

apply plugin: com.hearing.plugin.TransformPlugin
复制代码

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

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