字节码插桩技术---Transform配合ASM进行插桩(三)

字节码插桩技术---ASM的使用(一)

字节码插桩技术---Android项目实操(二)

字节码插桩技术---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版本的结构变化比较大,配置的时候总会有一些问题,我没有成功解决,如果有成功的小伙伴记得留言,请教请教~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值