本文已参与「新人创作礼」活动,一起开启掘金创作之路
这篇文章是对前一篇gradle系列第三篇——插件的一个补充分享,基于gradle插件的Transform接口,实现一个简单的android字节码修改插件,是对gradle自定义插件的一个实践
什么是Transform
Transfrom是Android gradle插件从1.5.0版本开始提供的接口,我们可以通过它去hook介入Android项目的打包流程
我们都知道一个apk的打包流程中包括两个关键步骤
- 将所有的java源码文件编译为class文件
- 再将这些class打包成dex 这两个步骤现在都是封装在android的gradle插件内部的,正常我们没办法去修改,但是有了TransForm之后,我们就可以在class打包成dex之前,获取到所有的class文件,有了这些class文件,我们自然就可以借助相关字节码修改工具(例如ASM)对class中的内容进行修改替换,也就是所谓的字节码插桩技术
再简单介绍下字节码插桩技术
字节码插桩技术就是修改class字节码文件,例如可以实现无感埋点,假设我们需要在所有页面的可点击组件的点击事件中加上埋点,正常我们可能需要在每个OnClick回调中一个个写埋点逻辑,但是有了字节码插桩,我们就可以不需要侵入到每个页面每个组件,直接在打包阶段通过字节码修改,批量操作加入埋点
Transform的使用
前面提到Transform的使用需要借助自定义gradle插件
首先按照gradle系列第三篇——插件自定义一个最基本的gradle插件
插件moudule的结构如图所示,其中CustomGradlePlugin里面就是继承自定义了一个Plugin,
class CustomGradlePlugin implements Plugin<Project>{
@Override
void apply(Project project) {
println "apply CustomGradlePlugin"
}
}
相比之前简单的自定义插件,主要在build.gradle脚本中增加了几个依赖项
apply plugin: 'groovy'
apply plugin: 'maven'
repositories {
mavenCentral()
google()
}
dependencies {
implementation localGroovy()
implementation gradleApi()
// 添加android gradle插件的依赖,因为需要用到这个库里面的Transform相关接口
implementation 'com.android.tools.build:gradle:4.2.2'
// ASM库依赖,用于修改字节码
implementation 'org.ow2.asm:asm:9.1'
implementation 'org.ow2.asm:asm-commons:9.1'
}
sourceSets {
main{
groovy {
srcDir 'src/main/groovy'
}
resources {
srcDir 'src/main/resources'
}
}
}
uploadArchives {
repositories.mavenDeployer {
repository(url: uri('../repo')) // 本地仓库路径
pom.groupId = "com.tu.gradle.plugin"// 唯一标识(通常为模块包名,也可以任意)
pom.artifactId = "CustomGradlePlugin"// 项目名称(通常为类库模块名称,也可以任意)
pom.version = "1.0.0"// 版本号
}
}
然后继承Transfrom实现一个自定义的CustomTransform
class CustomLogTransform extends Transform{
@Override
String getName() {
return "CustomLogTransform"
}
@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
}
}
Transfrom是一个抽象类,有4个接口必须要重写 getName 这个很简单就是返回当前transform的名字,可以任意
getInputTypes 指定需要处理的输入类型,什么是输入?Android打包过程中会产出多种类型文件的,例如class文件,资源文件,这个demo中我们需要修改的是class文件,所以这里指定为CONTENT_CLASS
getScopes 指定我们这个自定义的Transform可使用的范围,Scope有5种可选的类型
// 只有当前project,跟build.gradle的project对应
PROJECT(0x01),
// 只有当前project的子project
SUB_PROJECTS(0x04),
// 只有三方库
EXTERNAL_LIBRARIES(0x10),
// 测试代码
TESTED_CODE(0x20),
// 只提供本地或远程依赖项
PROVIDED_ONLY(0x40),
另外TransformManager通过这几种基本的scope的组合提供了一些复合类型,例如我们这demo使用的SCOPE_FULL_PROJECT就是包括了项目下所有的代码,包括依赖的三方库
isIncremental 控制是否支持增量编译,false表示全量编译
transform方法
除了上面必须重写的4个抽象方法,还有一个方法也必须重写,就是transform,我们需要通过transform方法去获取到class文件
@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
def inputs = transformInvocation.inputs
def outputProvider = transformInvocation.outputProvider
}
这里只是把函数的参数解析了出来,可以看到TransformInvocation类中有两个重要属性,一个是inputs,一个是outputProvider
inputs
inputs是一个类型为TransformInput的集合,他是所有输入文件的集合,TransformInput里面又包括两种输入类型
- DirectoryInput集合 这个集合包括了以源码形式参与构建的目录结构和源码文件
- JarInput集合 这个集合包括了以jar形式和aar形式参与构建的文件
outProvider
outputProvider是TransformOutputProvider类型的,处理过后的各种文件需要通过它输出到下一个transform
字节码修改
上面已经简单实现了一个Transfrom,在transfrom方法中可以去获取到输入inputs了,下面的关键就是把这些inputs中的class取出来,并根据需求去修改相关的class文件
字节码修改需要借助到一个关键的库ASM(字节码修改中涉及到的知识还比较多,后续也会针对字节码相关的知识学习分享一个系列,目前这篇侧重点在理解Transfrom,ASM等有个大概的了解就好)
这个demo我们主要会用到ASM框架中的两个基础类,ClassVistor,MethodVisitor
ClassVisitor
这个类就是ASM用来接收处理Class文件的入口,继承它实现一个自定义的ClassVisitor,直接上代码,主要看下注释
public class LogClassVisitor extends ClassVisitor {
// api是ASM的api版本,是一个枚举
// 接收一个Classvisitor类型参数,这里其实属于代理模式,后面用到的时候会讲到
public LogClassVisitor(int api, ClassVisitor classVisitor) {
super(api, classVisitor);
}
// 遍历class中的方法,会回调到这个方法
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
// 返回一个MethodVisitor,从字面意思就能看出用来hook方法的
MethodVisitor visitor = cv.visitMethod(access,name,descriptor,signature,exceptions);
// hook OnCreate方法
if(name.equals("onCreate")){
return new LogMethodVisitor(Opcodes.ASM5, visitor);
}
return visitor;
}
}
在上面实现的ClassVisitor中,visitMethod是用来遍历class文件里面的函数的,它需要返回一个MethodVisitor,所以我们继承这个MethodVisitor实现一个
MethodVisitor
class LogMethodVisitor extends MethodVisitor{
public LogMethodVisitor(int api, MethodVisitor methodVisitor) {
super(api, methodVisitor);
}
// 加入需要插入的逻辑代码
@Override
public void visitCode() {
super.visitCode();
mv.visitLdcInsn("ASM_CUSTOM_LOG");
mv.visitLdcInsn("custom transform log");
mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log","e",
"(Ljava/lang/String;Ljava/lang/String;)I",false);
mv.visitInsn(Opcodes.POP);
}
}
MethodVisitor很明显就是用来hook访问对应的方法,这里只是举个简单的例子,在onCreate方法中插入了一段日志
Transfrom与ASM结合
有了具体的操作字节码的逻辑,就可以将前面写好的transform模块跟这个串起来了,将截取到的class文件,传递给ClassVisitor去处理,主要看下注释
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
def inputs = transformInvocation.inputs
def outputProvider = transformInvocation.outputProvider
inputs.each {
// 遍历所有依赖性的jar或者aar里面的文件进行处理
it.jarInputs.each {jarInput->
// 需要将所有输入的jar里面的文件copy到输出目录,以交给下一下transform处理
File dest = outputProvider.getContentLocation(jarInput.name,jarInput.contentTypes,jarInput.scopes, Format.JAR)
FileUtils.copyFile(jarInput.file,dest)
}
// 遍历源码里面的文件进行处理
it.directoryInputs.each {directoryInput->
directoryInput.file.eachFileRecurse {file->
// 判断是class文件,将他交给自定义的ClassVisitor
if(file.absolutePath.endsWith(".class")){
// ClassReader读取class文件的内容
def classReader = new ClassReader(file.bytes)
// ClassWriter用于回写修改后的class内容
def classWriter = new ClassWriter(classReader,ClassWriter.COMPUTE_MAXS)
// ClassWriter其实也是ClassVisitor的子类,这样就可构造出一个自定义的ClassVisitor
def classVisitor = new LogClassVisitor(Opcodes.ASM5,classWriter)
// ClassVisitor处理class
classReader.accept(classVisitor,ClassReader.EXPAND_FRAMES)
// 处理后的数据写回文件
def bytes = classWriter.toByteArray()
def outputStream = new FileOutputStream(file.path)
outputStream.write(bytes)
outputStream.close()
}
}
// 需要将所有输入文件copy到输出,以交给下一下transform处理
File dest = outputProvider.getContentLocation(directoryInput.name,
directoryInput.contentTypes,directoryInput.scopes,Format.DIRECTORY)
FileUtils.copyDirectory(directoryInput.file,dest)
}
}
}
整个过程就是遍历所有的文件,过滤需要处理的Class文件交给ClassVisitor,处理完后放到输出目录,交由下一个transform处理,这样就完成了一个简单的字节码插桩插件
字节码插件的使用
只需要在app的gradle脚本中应用这个自定义的插件
apply plugin: 'com.tu.gradle.plugin'
执行一下assemble的任务,输出打包一个apk,运行看下日志
com.example.myapplication I/ASM_CUSTOM_LOG: custom transform log
结果符合预期
最后还可以通过jadx反编译再确认下
tips
运行之后发现了一个奇怪的bug Type kotlinx.coroutines.android.AndroidDispatcherFactory is defined multiple times
几番google也没找到具体原因,最后clean一下好了,有知道的大佬烦请解答一下
以上就是gradle系列Transform相关的分享,如果有写的不对的地方欢迎评论指正,感觉写的还不错的也欢迎评论点赞支持一下,感谢