字节码插桩技术---Transform配合ASM进行插桩(三)
上篇文章字节码插桩技术---Android项目实操(二)_紫气东来_life的博客-CSDN博客介绍了在build.gradle中如何使用ASM进行字节码处理。在build.gradle中进行插桩的话,会存在几个问题:
(1)对groovy语法要有一些了解(2)不同的gradle版本,所做的处理会有不同,比如打包dex的指令,对class文件的处理方式上等。这些问题上篇文章都有说过。今天将介绍一种新的方式:使用Transform的相关API配合ASM进行插桩。
一、创建插件Module
如图的项目文件结构
第一步:我们在和app同级的目录中创建一个文件夹buildSrc,不用new module,直接创建文件夹即可,文件夹名称为buildSrc
第二步:根据app的文件目录结构,依次创建src,main,java等目录,创建完之后同步一下,然后就可以看见文件夹颜色的变化
第三步:在java路径下,创建包路径,我这里是com.gzc.plugin,包路径的名称随意
第四步:在buildSrc中创建build.gradle文件,然后再dependencies中依赖gradle plugin,内容如下:
apply plugin: 'java'
repositories {
google()
mavenCentral()
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.tools.build:gradle:3.6.3'
}
sourceCompatibility = "7"
targetCompatibility = "7"
tasks.withType(JavaCompile) {
options.encoding = "UTF-8"
}
二、使用Transform进行编译的拦截
首先,我们创建一个plugin文件,内容如下:
package com.gzc.plugin;
import com.android.build.gradle.AppExtension;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
public class ASMPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getExtensions().getByType(AppExtension.class).registerTransform(new ASMTransform());
}
}
这个文件创建完成之后,我们就可以在其他项目的build.gradle中进行引入了,引入的方式如下:
apply plugin:com.gzc.plugin.ASMPlugin
在apply方法中,调用了registerTransform方法,注册了ASMTransform文件。这个ASMTransform其实就是一个Task,也就是说,注册成功之后,在build过程中的输出,会有我们这个自定义Task名称的输出。具体的输出内容,我们根据文件内容来说:
public class ASMTransform extends Transform {
@Override
public String getName() {
return "asm";
}
@Override
public Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS;
}
@Override
public Set<? super QualifiedContent.Scope> getScopes() {
return ImmutableSet.of(QualifiedContent.Scope.PROJECT, QualifiedContent.Scope.SUB_PROJECTS);
}
@Override
public boolean isIncremental() {
return false;
}
@Override
public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
super.transform(transformInvocation);
TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
outputProvider.deleteAll();
Collection<TransformInput> inputs = transformInvocation.getInputs();
for (TransformInput input : inputs) {
Collection<DirectoryInput> directoryInputs = input.getDirectoryInputs();
for (DirectoryInput directoryInput : directoryInputs) {
String dirName = directoryInput.getName();
File src = directoryInput.getFile();
String md5Name = DigestUtils.md5Hex(src.getAbsolutePath());
File dest = outputProvider.getContentLocation(
dirName + md5Name, directoryInput.getContentTypes(),
directoryInput.getScopes(), Format.DIRECTORY);
processInject(src, dest);
}
//jar包的处理
Collection<JarInput> jarInputs = input.getJarInputs();
for (JarInput jarInput : jarInputs) {
processJarInput(jarInput,transformInvocation.getOutputProvider());
}
}
}
void processJarInput(JarInput jarInput, TransformOutputProvider outputProvider) throws IOException {
File dest = outputProvider.getContentLocation(
jarInput.getFile().getAbsolutePath(),
jarInput.getContentTypes(),
jarInput.getScopes(),
Format.JAR);
FileUtils.copyFile(jarInput.getFile(), dest);
}
void processInject(File src, File dest) throws IOException {
String dir = src.getAbsolutePath();
FluentIterable<File> allFiles = FileUtils.getAllFiles(src);
for (File file : allFiles) {
if (!file.getAbsolutePath().endsWith(".class")) {
continue;
}
FileInputStream fis = new FileInputStream(file);
ClassReader classReader = new ClassReader(fis);
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
classReader.accept(new ClassVisitor(Opcodes.ASM7, classWriter) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
MethodVisitor methodVisitor = super.visitMethod(access, name, descriptor, signature, exceptions);
return new MyMethodVisitor(Opcodes.ASM7, methodVisitor, access, name, descriptor);
}
}, ClassReader.EXPAND_FRAMES);
byte[] newClassBytes = classWriter.toByteArray();
String absolutePath = file.getAbsolutePath();
String fullClassPath = absolutePath.replace(dir, "");
File outFile = new File(dest, fullClassPath);
FileUtils.mkdirs(outFile.getParentFile());
FileOutputStream fos = new FileOutputStream(outFile);
fos.write(newClassBytes);
fos.close();
}
}
}
1.getName方法
返回值写的是“asm”,那么在build的输出中就会有如下Task的输出:
transformClassesWithAsmForDebug就是我们自定义Task的输出,我们可以看见,这个Task是在dexBuilderDebug之前执行的,dexBuilderDebug上篇文章我们说了,是class文件打包为dex的Task。也就是说,我们在自定义Task中可以拿到相关的class。
2.getInputTypes方法
这个方法的含义就是要处理文件的类型,除了上面的TransformManager.CONTENT_CLASS外,一共有三种:
TransformManager.CONTENT_CLASS:拦截class以及jar文件。自己项目中的源文件都会被编译为class文件,而三方库中的源文件会以jar的形式输出到这里
TransformManager.CONTENT_JARS:除了上面的class以及jar文件外,还有会拦截resources中的相关资源文件,这种资源文件也是以jar包的形式存在的
TransformManager.CONTENT_RESOURCES:之拦截resources中的相关资源文件
3.getScopes方法
这个方法的意思就是文件的作用域,可以使用系统提供的,比如
TransformManager.SCOPE_FULL_PROJECT:表示作用域为当前的module,依赖的子module以及三方库
TransformManager.PROJECT_ONLY:作用域为当前的module。
当然也可以自己组合,我使用的就是组合的方式,作用域为当前module以及依赖的子module
4.isInsremental方法
表示是否增量,如果设置为true,那么只会拦截有增量或减量变化的文件,我这里设置的是false,所以不会考虑增量变化,所有文件都会输出
5.transform方法
核心方法,要处理的文件会在这里被拦截,以及处理后传递给下一个Task。其中transformInvocation.getInputs方法拿到的是所有符合条件文件的目录;所有处理完之后的文件,都会重新填充到transformInvocation.getOutputProvider方法的返回值中。
6.processInject方法
这个方法是我们自定义的方法,其中的逻辑不难,就是遍历每一个class文件,然后使用ASM进行字节码操作;之后将修改后的class文件填充到指定目录dest中。而这个dest的路径为:app/build/intermediates/transforms,自定义Task生成的文件都会存在这里,这个路径中的文件会被打包进dex
7.processJarInput方法
这个方法也是我们自定义的方法,主要是处理Jar文件。这个方法需要我们注意一下:我们从代码逻辑上看,仅仅是改变了Jar的路径,对其中的class文件并没处理。而这里的Jar包其实是R.jar,里面包含了资源索引。开始的时候,我也没有去处理,因为我想的是应该和上篇文章一样,只处理想要处理的文件就行了。其实不是,我们不想要处理的文件,也是需要重新填充到transformInvocation.getOutputProvider这里的,作为下一个任务的输入。
8.MyMethodVisitor类
这个类上篇文章有,可以从上篇文章copy
三、Transform的其他用法
1.使用引号的方式依赖插件
我们上面所说的依赖方式和我们看见的不同,我们经常看见是使用引号的方式,比如:
apply plugin: 'com.android.application'
当然,我们也可以通过注册的方式,将我们的插件文件注册在配置文件中,从而使用引号的方式进行引用,如图:
第一步:创建如图中的目录结构,以及后缀为.properties的文件
第二步:在文件中写如图的配置
之后,我们就可以通过双引号的方式进行引用了,双引号中的名称就是.properties的文件名,这个名称是随意命名的。
2.插件拓展的使用
图片中的代码大家应该都很熟悉,这就是插件的拓展。比如:android,compileSdkVersion等,我们自定义类似这些的字段,如下:
对上面我们所说的ASMPlugin进行改造,如下:
public class ASMPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
//注释1
project.getExtensions().create("gzc", GzcExt.class);
project.afterEvaluate(new Action<Project>() {
@Override
public void execute(Project project) {
GzcExt ext = project.getExtensions().findByType(GzcExt.class);
System.out.println("test:"+ext.isTest());
}
});
}
}
注释1这里的“gzc”,就是类似android的拓展字段;GzcExt是一个类,其中的字段就是拓展中使用的字段:
public class GzcExt {
private boolean test;
public boolean isTest() {
return test;
}
public void setTest(boolean test) {
this.test = test;
}
}
创建完成之后,我们就可以在其他的build.gradle中写如下的插件拓展了:
gzc{
test true
}
四、注意
个人建议使用的gradle版本在7.0以下,因为gradle7版本的结构变化比较大,配置的时候总会有一些问题,我没有成功解决,如果有成功的小伙伴记得留言,请教请教~