编译插桩利用ASM 插入字节码

使用ASM,插入字节码到Activity文件


简单记录一下ASM在此需求中的实现插入字节码

一、 ASM

ASM 是一个Java字节码操控框架, 可以用来改变或者增强现有类的功能,可以通过解析.class文件中的字节码,经过一些处理,生成新的字节码

1.1 在这里仅完成需求,需要主要的几个类:

  1. ClassReader 负责解析 .class 文件中的字节码,并将所有字节码传递给 ClassWriter
  2. ClassVisitor: 负责访问.class文件的各个元素,可以解析或者修改.class文件的内容
  3. ClassWriter:继承自 ClassVisitor,它是生成字节码的工具类,负责将修改后的字节码输出为 byte 数组

1.2 添加ASM到自定义gralde组件的中

  • 添加依赖
 // ASM 相关依赖
    implementation 'org.ow2.asm:asm:7.1'
    implementation 'org.ow2.asm:asm-commons:7.1'

  • 创建自定义的Visitor类

记住这个类文件不要使用kotlin来写,用Java来写,具体原因在后面专门记录

public class CustomClassVisitor extends ClassVisitor {
    private String className;
    private String superName;

    public CustomClassVisitor(ClassVisitor cv) {
        super(Opcodes.ASM5, cv);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        super.visit(version, access, name, signature, superName, interfaces);
        this.className = name;
        this.superName = superName;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        System.out.println("ClassVisitor visitMethod name--->" + name + ", superName" + superName);
        MethodVisitor mv = cv.visitMethod(access, name, descriptor, signature, exceptions);
        // 判断是不是继承于AppCompatActivity,这个地方是androidx的,看自己项目的需求,但是涉及到第三方的jar包,一般都是连个都判断,找到Activity类几方法
        if (superName.equals("androidx/appcompat/app/AppCompatActivity")) {
            if (name.startsWith("onCreate")) {
                return new CustomMethodVisitor(mv, className, name);
            }
        }
        return mv;
    }

    @Override
    public void visitEnd() {
        super.visitEnd();
    }
}

visitMethod方法中 判断是不是继承于AppCompatActivity,这个地方是androidx的,看自己项目的需求,但是涉及到第三方的jar包,一般都是连个都判断,找到Activity类几方法

  • 真正的执行插入字节码的代码是在自定义的MethodVisitor中
public class CustomMethodVisitor extends MethodVisitor {
    private String className;
    private String methodName;

    public CustomMethodVisitor(MethodVisitor mv, String className, String methodName) {
        super(Opcodes.ASM5, mv);
        this.className = className;
        this.methodName = methodName;
    }

    @Override
    public void visitCode() {
        super.visitCode();
        System.out.println("MethodVisitor visitCode--->");
        mv.visitLdcInsn("TAG");
        mv.visitLdcInsn(className + "---->" + methodName);
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "i",
                "(Ljava/lang/String;Ljava/lang/String;)I", false);
        mv.visitInsn(Opcodes.POP);
    }
}

visitCode 方法中进行插入字节码 ,ASM 都是直接以字节码指令的方式进行操作的

二、将插入字节码的类在transform中进行绑定

void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        // 拿到所有的class文件
        Collection<TransformInput> transformInputs = transformInvocation.inputs
        TransformOutputProvider outputProvider = transformInvocation.outputProvider
        if (outputProvider != null) {
            outputProvider.deleteAll()
        }
        transformInputs.each { TransformInput transformInput ->
        
            // // 遍历directoryInputs(文件夹中的class文件) directoryInputs代表着以源码方式参与项目编译的所有目录结构及其目录下的源码文件
            //            // 比如我们手写的类以及R.class、BuildConfig.class以及MainActivity.class等
            transformInput.directoryInputs.each { DirectoryInput directoryInput ->
                File dir = directoryInput.file
                if (dir) {
                    dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) { File file ->
                        System.out.println("find class: " + file.name)
                        // 对Class 文件进行读取与解析
                        ClassReader classReader = new ClassReader(file.bytes)
                        // class 文件写入
                        ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS)
                        // 访问class 文件相应的内容、解析某一个结构就会通知到ClassVisitor的相应方法
                        CustomClassVisitor classVisitor = new CustomClassVisitor(classWriter)
                        // 依次调用ClassVisitor 接口的各个方法
                        classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES)
                        // 将最终修改的字节码以byte数组形式返回
                        byte[] bytes = classWriter.toByteArray()
                        // 通过文件流写入方式覆盖原先的内容,实现class文件的改写
                        FileOutputStream fileOutputStream = new FileOutputStream(file.path)
                        fileOutputStream.write(bytes)
                        fileOutputStream.close()
                    }
                    // 处理完输入之后吧输出传给下一个文件
                    def dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
                    FileUtils.copyDirectory(directoryInput.file, dest)
                }
            }
        }

    }

这就完成了字节码插桩, 进行编译,并且以来在app 的gradle 中添加依赖,如果是gradle3.6以下版本,通过上述方法是可以的,

2.1 gradle3.6 及以上 包适配,

// 运行保存
Landroidx/appcompat/R$drawable;

gradle3.6+将R文件单独编译成了一个jar包,

所有找不到,需要将R.jar单独进行复制

transformInput.jarInputs.each { JarInput jarInput ->
                File file = jarInput.file
                System.out.println("find jar input: " + file.name)
                def dest = outputProvider.getContentLocation(jarInput.name,
                        jarInput.contentTypes,
                        jarInput.scopes, Format.JAR)
                FileUtils.copyFile(file, dest)
            }

然后在编译,运行

成功后日志打印:

2020-04-28 14:12:59.272 10953-10953/com.kpa.compiletheplugpile I/TAG: com/kpa/compiletheplugpile/MainActivity---->onCreate

项目代码地址

猜你喜欢

转载自blog.csdn.net/qq_32648731/article/details/105814746