dex工具与transform_初识Android Gradle编译之Transform

本文介绍了Android Gradle的Transform API,从1.5版本开始,Transform API允许开发者在class文件转为dex之前进行操作。通过实现Transform类,设置输入类型、作用域和增量编译,可以自定义处理class文件。文中详细讲解了Transform的各个方法,如getName、getInputTypes、getScopes和isIncremental,并通过示例展示了如何使用Javassist进行字节码操作,将代码注入到MainActivity中。此外,还探讨了Transform的注意事项,如无法处理Dex和不能使用自身等。Transform API为开发者提供了编译期定制字节码的强大工具。
摘要由CSDN通过智能技术生成

什么是Transform

从android-build-tool:gradle:1.5开始,gradle插件包含了一个叫Transform的API,这个API允许第三方插件在class文件转为为dex文件前操作编译好的class文件,这个API的目标就是简化class文件的自定义的操作而不用对Task进行处理。

本文主要学习 Transform API 的基本知识,然后借助 javassist 来完成一个简单的字节码操作。

先来看 Transform 类:

public abstract class Transform

它是一个抽象类,自定义 Transform 时必须继承 Transform 类,并实现它的几个方法:

getName 方法

public abstract String getName();

用于指明 Transform 的名字,也对应了该 Transform 所代表的 Task 名称,例如:

// 设置自定义的Transform对应的Task名称

// 类似:transformClassesWithPreDexForXXX

// 这里应该是:transformClassesWithInjectTransformForxxx

@Override

String getName() {

return 'InjectTransform'

}

示例中给 Transform 取名:InjectTransform ,编译运行后,可以在 Android Studio 中查到生成的 Task 。

image.png

getInputTypes 方法

public abstract Set getInputTypes();

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

// 代表 javac 编译成的 class 文件,常用

public static final Set CONTENT_CLASS;

public static final Set CONTENT_JARS;

// 这里的 resources 单指 java 的资源

public static final Set CONTENT_RESOURCES;

public static final Set CONTENT_NATIVE_LIBS;

public static final Set CONTENT_DEX;

public static final Set CONTENT_DEX_WITH_RESOURCES;

public static final Set DATA_BINDING_BASE_CLASS_LOG_ARTIFACT;

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

getScopes 方法

public abstract Set super Scope> getScopes();

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

// 注意,不同版本值不一样

public static final Set EMPTY_SCOPES = ImmutableSet.of();

public static final Set PROJECT_ONLY;

public static final Set SCOPE_FULL_PROJECT; // 常用

public static final Set SCOPE_FULL_WITH_IR_FOR_DEXING;

public static final Set SCOPE_FULL_WITH_FEATURES;

public static final Set SCOPE_FULL_WITH_IR_AND_FEATURES;

public static final Set SCOPE_FEATURES;

public static final Set SCOPE_FULL_LIBRARY_WITH_LOCAL_JARS;

public static final Set SCOPE_IR_FOR_SLICING;

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

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

isIncremental 方法

public abstract boolean isIncremental();

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

transform 方法

/** @deprecated */

@Deprecated

public void transform(Context context, Collection inputs, Collection referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {

}

public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {

this.transform(transformInvocation.getContext(), transformInvocation.getInputs(), transformInvocation.getReferencedInputs(), transformInvocation.getOutputProvider(), transformInvocation.isIncremental());

}

重写任意一个方法即可。其中,inputs 是该 Transform 要消费的输入流,有两种格式:jar 和目录格式;referencedInputs 集合仅供参考,不应进行转换,它是受 getReferencedScopes 方法控制的;outputProvider 是用来获取输出目录的,我们要将操作后的文件复制到输出目录中。

TransformInput 类

public interface TransformInput {

Collection getJarInputs();

Collection getDirectoryInputs();

}

所谓 Transform 就是对输入的 class 文件转变成目标字节码文件,TransformInput 就是这些输入文件的抽象。目前它包括两部分:DirectoryInput 集合与 JarInput 集合。

DirectoryInput 代表以源码方式参与项目编译的所有目录结构及其目录下的源码文件,可以借助于它来修改输出文件的目录结构以及目标字节码文件。

JarInput 代表以 jar 包方式参与项目编译的所有本地 jar 包或远程 jar 包,可以借助它来动态添加 jar 包。

TransformOutputProvider 类

public interface TransformOutputProvider {

void deleteAll() throws IOException;

File getContentLocation(String var1, Set var2, Set super Scope> var3, Format var4);

}

调用 getContentLocation 获取输出目录,例如:

// 获取输出目录

def dest = outputProvider.getContentLocation(directoryInput.name,

directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)

image.png

每个 Transform 其实都是一个 Gradle 的 Task , Android 编译器中的 TaskManager 会将每个 Transform 串联起来。第一个 Transform 接收来自 javac 编译的结果,以及拉取到本地的第三方依赖和 resource 资源。这些编译的中间产物在 Transform 链上流动,每个 Transform 节点都可以对 class 进行处理再传递到下一个 Transform 。我们自定义的 Transform 会插入到链的最前面,可以在 TaskManager 类的 createPostCompilationTasks 方法中找到相关逻辑:

public void createPostCompilationTasks(VariantScope variantScope) {

...

TransformManager transformManager = variantScope.getTransformManager();

...

// 获取自定义 Transform 列表

List customTransforms = extension.getTransforms();

List> customTransformsDependencies = extension.getTransformsDependencies();

int i = 0;

// 循环添加

for(int count = customTransforms.size(); i < count; ++i) {

Transform transform = (Transform)customTransforms.get(i);

List deps = (List)customTransformsDependencies.get(i);

transformManager.addTransform(this.taskFactory, variantScope, transform, (PreConfigAction)null, (taskx) -> {

if (!deps.isEmpty()) {

taskx.dependsOn(new Object[]{deps});

}

}, (taskProvider) -> {

if (transform.getScopes().isEmpty()) {

TaskFactoryUtils.dependsOn(variantScope.getTaskContainer().getAssembleTask(), taskProvider);

}

});

}

}

以上是 Transform 的数据流动原理,下面再说下 Transform 的输入数据的过滤机制。

Transform 的数据输入 key 通过 Scope 和 ContentType 两个维度进行过滤。ContentType 就是数据类型,在开发中一般只能使用 CLASSES 和 RESOURCES 两种类型,这里的 CLASSES 已经包含了 class 文件和 jar 包。其他的一些类型如 DEX 是留给 Android 编译器的,我们无法使用。至于 Scope ,开发可用的相对较多(详细见 TransformManager 类),处理 class 字节码时一般使用 SCOPE_FULL_PROJECT 。

Javassist 操作字节码

说完了 Transform 的理论,我们来实际操作一下,编写自定义 Transform 来给类文件插入一行代码。

示例:

利用 Javassist 在 MainActivity 的 onCreate 方法的最后插入一行 Toast 语句。

1.创建自定义插件 Module

2.引入 Transform API 和 Javassist 依赖

dependencies {

...

compile 'com.android.tools.build:gradle:3.3.1'

compile group: 'org.javassist', name: 'javassist', version: '3.22.0-GA'

}

Transform API 和 Javassist 需要单独依赖,这里直接依赖 gradle 是因为其包含的 API 会更加丰富。注意:Transform API 的依赖包经历过修改,从 transform-api 改成了 gradle-api ,大家可以在 Jcenter 中找到相应版本。

3.实现自定义 Transform

/**

* 定义一个Transform

*/

class InjectTransform extends Transform {

private Project mProject

// 构造函数,我们将Project保存下来备用

InjectTransform(Project project) {

this.mProject = project

}

// 设置我们自定义的Transform对应的Task名称

// 类似:transformClassesWithPreDexForXXX

// 这里应该是:transformClassesWithInjectTransformForxxx

@Override

String getName() {

return 'InjectTransform'

}

// 指定输入的类型,通过这里的设定,可以指定我们要处理的文件类型

// 这样确保其他类型的文件不会传入

@Override

Set getInputTypes() {

return TransformManager.CONTENT_CLASS

}

// 指定Transform的作用范围

@Override

Set super QualifiedContent.Scope> getScopes() {

return TransformManager.SCOPE_FULL_PROJECT

}

// 当前Transform是否支持增量编译

@Override

boolean isIncremental() {

return false

}

// 核心方法

// inputs是传过来的输入流,有两种格式:jar和目录格式

// outputProvider 获取输出目录,将修改的文件复制到输出目录,必须执行

@Override

void transform(Context context, Collection inputs,

Collection referencedInputs, TransformOutputProvider outputProvider,

boolean isIncremental) throws IOException, TransformException, InterruptedException {

println '--------------------transform 开始-------------------'

// Transform的inputs有两种类型,一种是目录,一种是jar包,要分开遍历

inputs.each {

TransformInput input ->

// 遍历文件夹

//文件夹里面包含的是我们手写的类以及R.class、BuildConfig.class以及R$XXX.class等

input.directoryInputs.each {

DirectoryInput directoryInput ->

// 注入代码

MyInjectByJavassit.injectToast(directoryInput.file.absolutePath, mProject)

// 获取输出目录

def dest = outputProvider.getContentLocation(directoryInput.name,

directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)

println("directory output dest: $dest.absolutePath")

// 将input的目录复制到output指定目录

FileUtils.copyDirectory(directoryInput.file, dest)

}

//对类型为jar文件的input进行遍历

input.jarInputs.each {

//jar文件一般是第三方依赖库jar文件

JarInput jarInput ->

// 重命名输出文件(同目录copyFile会冲突)

def jarName = jarInput.name

println("jar: $jarInput.file.absolutePath")

def md5Name = DigestUtils.md5Hex(jarInput.file.absolutePath)

if (jarName.endsWith('.jar')) {

jarName = jarName.substring(0, jarName.length() - 4)

}

def dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)

println("jar output dest: $dest.absolutePath")

FileUtils.copyFile(jarInput.file, dest)

}

}

println '---------------------transform 结束-------------------'

}

}

4.使用 Javassist 实现代码注入逻辑

/**

* 借助 Javassit 操作 Class 文件

*/

class MyInjectByJavassit {

private static final ClassPool sClassPool = ClassPool.getDefault()

/**

* 插入一段Toast代码

* @param path

* @param project

*/

static void injectToast(String path, Project project) {

// 加入当前路径

sClassPool.appendClassPath(path)

// project.android.bootClasspath 加入android.jar,不然找不到android相关的所有类

sClassPool.appendClassPath(project.android.bootClasspath[0].toString())

// 引入android.os.Bundle包,因为onCreate方法参数有Bundle

sClassPool.importPackage('android.os.Bundle')

File dir = new File(path)

if (dir.isDirectory()) {

// 遍历文件夹

dir.eachFileRecurse { File file ->

String filePath = file.absolutePath

println("filePath: $filePath")

if (file.name == 'MainActivity.class') {

// 获取Class

// 这里的MainActivity就在app模块里

CtClass ctClass = sClassPool.getCtClass('com.apm.windseeker.MainActivity')

println("ctClass: $ctClass")

// 解冻

if (ctClass.isFrozen()) {

ctClass.defrost()

}

// 获取Method

CtMethod ctMethod = ctClass.getDeclaredMethod('onCreate')

println("ctMethod: $ctMethod")

String toastStr = """ android.widget.Toast.makeText(this,"我是被插入的Toast代码~!!",android.widget.Toast.LENGTH_SHORT).show();

"""

// 方法尾插入

ctMethod.insertAfter(toastStr)

ctClass.writeFile(path)

ctClass.detach() //释放

}

}

}

}

}

5.将 Transform 注册到 Android 插件中

/**

* 定义插件,加入Transform

*/

class TransformPlugin implements Plugin {

@Override

void apply(Project project) {

// 获取Android扩展

def android = project.extensions.getByType(AppExtension)

// 注册Transform,其实就是添加了Task

android.registerTransform(new InjectTransform(project))

// 这里只是随便定义一个Task而已,和Transform无关

project.task('JustTask') {

doLast {

println('InjectTransform task')

}

}

}

}

这里先通过 AppExtension 获取 Android 扩展,然后调用 registerTransform 方法添加自定义的 Transform 。

6.发布插件并使用

/* 自定义插件:利用Transform向MainActivity中插入代码 */

apply plugin: 'com.happy.customplugin.transform'

运行后,可以在 build/intermediates/transforms 目录下找到自定义的 Transform :

image.png

这里的 jar 包名字是数字递增的,这是正常的,其命名逻辑可以在 IntermediateFolderUtils 类的 getContentLocation 方法中找到。我们直接看 MainActivity.class 文件:

image.png

可以看到成功注入了一行 Toast 语句。运行 APP 也能正常弹出 Toast 。

Transform 的注意点

1.自定义 Transform 无法处理 Dex ;

2.自定义 Transform 无法使用自定义 Transform ;

3.可以使用 isIncremental 来支持增量编译以及并发处理来加快 Transform 编译速度;

4.Transform 只能在全局注册,并将其应用于所有变体(variant)。

总结

Transform 简单来看就是一个 Task ,只不过 Android 在这个 Task 中给我们提供了一个修改 Class 字节码的契机。我们可以根据自己的业务需求进行字节码操作。文中利用 Javassist 写的示例很简单,像 APM 这种功能强大的 SDK ,它的字节码处理逻辑会很复杂,可能会使用到更强大的 ASM 字节码处理工具。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值