咱们已经学习了如何自定义 Gradle 插件以及 Android 插件的基本知识。那咱们自定义 Gradle 插件用来干什么呢?总不能只是定义一些简单 Task 吧,那就有点大材小用了。这个时候,Android 插件就派上用场了。由于,从 1.5.0-beta1 版本开始,Android 插件中包含了 Transform API ,它容许第三方插件在将编译后的类文件转换为 dex 文件以前对其进行操做。java
本文主要学习 Transform API 的基本知识,而后借助 javassist 来完成一个简单的字节码操做。android
先来看 Transform 类:git
public abstract class Transform 复制代码
它是一个抽象类,自定义 Transform 时必须继承 Transform 类,并实现它的几个方法:github
public abstract String getName();
复制代码
用于指明 Transform 的名字,也对应了该 Transform 所表明的 Task 名称,例如:api
// 设置自定义的Transform对应的Task名称
// 相似:transformClassesWithPreDexForXXX
// 这里应该是:transformClassesWithInjectTransformForxxx
@Override
String getName() {
return 'InjectTransform'
}
复制代码
示例中给 Transform 取名:InjectTransform ,编译运行后,能够在 Android Studio 中查到生成的 Task 。并发
public abstract Set<ContentType> getInputTypes();
复制代码
用于指明 Transform 的输入类型,能够做为输入过滤的手段。在 TransformManager 类中定义了不少类型:app
// 表明 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 文件。ide
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 组成的资源流。
public abstract boolean isIncremental();
复制代码
指明该 Transform 是否支持增量编译。须要注意的是,即便返回了 true ,在某些状况下运行时,它仍是会返回 false 的。
/** @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 是用来获取输出目录的,咱们要将操做后的文件复制到输出目录中。
一个简单的接口类:
public interface TransformInput {
Collection<JarInput> getJarInputs();
Collection<DirectoryInput> getDirectoryInputs();
}
复制代码
所谓 Transform 就是对输入的 class 文件转变成目标字节码文件,TransformInput 就是这些输入文件的抽象。目前它包括两部分:DirectoryInput 集合与 JarInput 集合。
DirectoryInput 表明以源码方式参与项目编译的全部目录结构及其目录下的源码文件,能够借助于它来修改输出文件的目录结构以及目标字节码文件。
JarInput 表明以 jar 包方式参与项目编译的全部本地 jar 包或远程 jar 包,能够借助它来动态添加 jar 包。
也是一个简单的接口:
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 表明自定义 Transform ,蓝色的表明系统的 Transform 。
每一个 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 。
说完了 Transform 的理论,咱们来实际操做一下,编写自定义 Transform 来给类文件插入一行代码。
示例:
利用 Javassist 在 MainActivity 的 onCreate 方法的最后插入一行 Toast 语句。
步骤一:建立自定义插件 Module
参照以前的文章Gradle 学习之插件中的方法建立自定义插件便可,这里直接给图:
步骤二:引入 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 中找到相应版本。
步骤三:实现自定义 Transform
这里直接贴代码了,其 API 和原理已经在上文中说过了。
/**
* 定义一个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 结束-------------------'
}
}
复制代码
步骤四:使用 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() //释放
}
}
}
}
}
复制代码
步骤五:将 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 。
步骤六:发布插件并使用
/* 自定义插件:利用Transform向MainActivity中插入代码 */
apply plugin: 'com.happy.customplugin.transform'
复制代码
运行后,能够在 build/intermediates/transforms 目录下找到自定义的 Transform :
这里的 jar 包名字是数字递增的,这是正常的,其命名逻辑能够在 IntermediateFolderUtils 类的 getContentLocation 方法中找到。咱们直接看 MainActivity.class 文件:
能够看到成功注入了一行 Toast 语句。运行 APP 也能正常弹出 Toast 。
Transform 简单来看就是一个 Task ,只不过 Android 在这个 Task 中给咱们提供了一个修改 Class 字节码的契机。咱们能够根据本身的业务需求进行字节码操做。文中利用 Javassist 写的示例很简单,像 APM 这种功能强大的 SDK ,它的字节码处理逻辑会很复杂,可能会使用到更强大的 ASM 字节码处理工具。
本文示例代码已放在 zjxstar 的 GitHub。