什么是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 。
getInputTypes 方法
public abstract Set<ContentType> 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;
其中,很多类型是不允许自定义 Transform 来处理的,我们常使用 CONTENT_CLASS 来操作 Class 文件。
getScopes 方法
public abstract Set<? super Scope> 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 方法
public abstract boolean isIncremental();
指明该 Transform 是否支持增量编译。需要注意的是,即使返回了 true ,在某些情况下运行时,它还是会返回 false 的。
transform 方法
/** @deprecated */
@Deprecated
public void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> 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<JarInput> getJarInputs();
Collection<DirectoryInput> 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<ContentType> var2, Set<? super Scope> var3, Format var4);
}
调用 getContentLocation 获取输出目录,例如:
// 获取输出目录
def dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
每个 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<Transform> customTransforms = extension.getTransforms();
List<List<Object>> customTransformsDependencies = extension.getTransformsDependencies();
int i = 0;
// 循环添加
for(int count = customTransforms.size(); i < count; ++i) {
Transform transform = (Transform)customTransforms.get(i);
List<Object> 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<QualifiedContent.ContentType> 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<TransformInput> inputs,
Collection<TransformInput> 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<Project> {
@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 :
这里的 jar 包名字是数字递增的,这是正常的,其命名逻辑可以在 IntermediateFolderUtils 类的 getContentLocation 方法中找到。我们直接看 MainActivity.class 文件:
可以看到成功注入了一行 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 字节码处理工具。