首页 > 其他分享 >注解的使用(二):插桩,编译后处理筛选

注解的使用(二):插桩,编译后处理筛选

时间:2023-06-19 10:37:56浏览次数:51  
标签:文件 字节 插桩 public 注解 后处理 class ASM String


什么是插桩?

插桩就是将一段代码插入或者替换原本的代码。字节码插桩顾名思义就是在我们编写的源码编译成字节码(Class)后,在Android下生成dex之前修改Class文件,修改或者增强原有代码逻辑的操作。

QQ空间曾经发布的《热修复解决方案》中利用 Javaassist库实现向类的构造函数中插入一段代码解决 CLASS_ISPREVERIFIED问题。包括了Instant Run的实现以及参照Instant Run实现的热修复美团Robus等都利用到了插桩技术。

字节码操作框架

QQ空间使用了 Javaassist 来进行字节码插桩,除了 Javaassist 之外还有一个应用更为广泛的 ASM 框架同样也是字节码操作框架,Instant Run包括 AspectJ 就是借助 ASM来实现各自的功能。

字节码操作框架的作用在于生成或者修改Class文件,因此在Android中字节码框架本身是不需要打包进入APK的,只有其生成/修改之后的Class才需要打包进入APK中。它的工作时机为Android打包流程中的生成Class之后,打包dex之前。

Android打包流程图:

注解的使用(二):插桩,编译后处理筛选_字节码

通过上图可知,只要在图中红色箭头处拦截(生成class文件之后,dex文件之前),就可以拿到当前应用程序中所有的.class文件,再去借助ASM之类的库,就可以遍历这些.class文件中所有方法,再根据一定的条件找到需要的目标方法,最后进行修改并保存,就可以插入指定代码。


ASM 使用

ASM是一个字节码操作库,它可以直接修改已经存在的class文件或者生成class文件。ASM提供了一些便捷的功能来操作字节码内容。与其它字节码操作框架(比如:AspectJ等)相比,ASM更偏向于底层,它是直接操作字节码的,在设计上相对更小、更快,所以在性能上更好,而且几乎可以任意修改字节码。

字节码查看方式

由于class文件本质是16进制数据,所以任意的16进制编辑器都可以查看,如以下方式:

  1. 可以通过16进制编辑器查看: 010 Editor
  2. 终端命令行
#打开class文件:
vim xx.class
#然后输入,就可以显示16进制的class文件了
:%!xxd
#字节码数据对应的指令可以通过javap指令查看
javap -v xx.class
  1. Android Studio 插件 ASM Bytecode Viewer 快捷转换字节码

ASM Bytecode Viewer 是直接查看字节码,没有 ASM Bytecode Outline 方便,它可以直接查看由 ASM API 写好的代码,可以复制使用。

ASM可以直接从 jcenter()仓库中引入,进入 https://bintray.com/ 搜索 org.ow2.asm

ASM Core API提供了3个类来操作字节码,分别是:

  • ClassReader : 对具体的class文件进行读取与解析;
  • ClassWriter : 将修改后的class文件通过文件流的方式覆盖掉原来的class文件,从而实现class修改;
  • ClassVisitor : 可以访问class文件的各个部分,比如方法、变量、注解等,这也是修改原代码的地方。

注意:ClassReader解析class文件过程中,解析到某个结构就会通知到ClassVisitor内部的相应方法(比如:解析到方法时,就会回调ClassVisitor.visitMethod方法)。

Demo (插桩式时长统计)
1. 创建自定义Gradle插件 ,并进行引用,插件中build.gradle文件配置如下
apply plugin: 'java'

dependencies {
    implementation gradleApi() // 必须
    // 如果要使用android的API,需要引用这个,实现Transform的时候会用到
    implementation 'com.android.tools.build:gradle:3.5.0'

    // 导入ASM
    implementation 'org.ow2.asm:asm:7.2'
    implementation 'org.ow2.asm:asm-commons:7.2'
}
repositories {
    google() // gradle 必须
    jcenter()
    mavenCentral() // 必须

}
// 指定编码,Java方式容易有乱码现象
tasks.withType(JavaCompile) {
    options.encoding = "UTF-8"
}
2. 在插件中注册监听,如下:
public class MyPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        // 注册 Transform, AppExtension 依赖 gradle,所以该模块需要导入 gradle
        AppExtension appExtension = project.getExtensions().getByType(AppExtension.class);
        appExtension.registerTransform(new MyTransform());
    }
}
3. 在自定义 Transform 中进行扫描所有类文件
public class MyTransform extends Transform {

    /** 当前Transform名称 */
    @Override
    public String getName() {
        return MyTransform.class.getSimpleName();
    }

    /** 输入文件类型,有CLASSES和RESOURCES */
    @Override
    public Set<QualifiedContent.ContentType> getInputTypes() {
        return TransformManager.CONTENT_CLASS;
    }

    /**
     * 指Transform要操作内容的范围,官方文档Scope有7种类型:
     * <p>
     * EXTERNAL_LIBRARIES        只有外部库
     * PROJECT                   只有项目内容
     * PROJECT_LOCAL_DEPS        只有项目的本地依赖(本地jar)
     * PROVIDED_ONLY             只提供本地或远程依赖项
     * SUB_PROJECTS              只有子项目。
     * SUB_PROJECTS_LOCAL_DEPS   只有子项目的本地依赖项(本地jar)。
     * TESTED_CODE               由当前变量(包括依赖项)测试的代码
     * SCOPE_FULL_PROJECT        整个项目
     */
    @Override
    public Set<? super QualifiedContent.Scope> getScopes() {
        return TransformManager.SCOPE_FULL_PROJECT;
    }

    /** 指明当前Transform是否支持增量编译 */
    @Override
    public boolean isIncremental() {
        return false;
    }

    /** transform进行干预文件 */
    @Override
    public void transform(TransformInvocation transformInvocation) throws TransformException,
            InterruptedException, IOException {
        super.transform(transformInvocation);
        // inputs中是传过来的输入流,其中有两种格式,一种是jar包格式一种是目录格式。
        Collection<TransformInput> inputs = transformInvocation.getInputs();
        // 获取到输出目录,最后将修改的文件复制到输出目录,这一步必须做不然编译会报错
        TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
        // 循环遍历输入流
        for (TransformInput input : inputs) {
            // 处理Jar中的class文件
            for (JarInput jarInput : input.getJarInputs()) {
                File dest = outputProvider.getContentLocation(
                        jarInput.getName(),
                        jarInput.getContentTypes(),
                        jarInput.getScopes(),
                        Format.JAR);
                // 将修改过的字节码copy到dest,就可以实现编译期间干预字节码的目的了
                FileUtils.copyFile(jarInput.getFile(), dest);
            }
            // 处理文件目录下的class文件
            for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
                // 处理文件目录下的class文件
                handleDirectoryInput(directoryInput, outputProvider);
            }
        }
    }

    /** 临时文件集合 */
    private List<File> mTemporaryFiles = new ArrayList<>();

    /** 处理文件目录下的class文件 */
    private void handleDirectoryInput(DirectoryInput directoryInput, TransformOutputProvider outputProvider) throws IOException {
        // 列出目录所有文件(包含子文件夹,子文件夹内文件)
        File dir = directoryInput.getFile();

        // 判断是否为目录
        if (directoryInput.getFile().isDirectory()) {
            // 查找目录下面所有的文件
            mTemporaryFiles.clear();
            traverseToFindFiles(dir);
            // 遍历所有文件
            for (File file : mTemporaryFiles) {
                // 处理相应文件
                processingTheCorrespondingFile(file);
            }
        }
        // 判断是否为文件
        else if (dir.isFile()) {
            // 处理相应文件
            processingTheCorrespondingFile(dir);
        } else {
            return;
        }
        // Transform 拷贝文件到 transforms 目录
        File dest = outputProvider.getContentLocation(
                directoryInput.getName(),
                directoryInput.getContentTypes(),
                directoryInput.getScopes(),
                Format.DIRECTORY);
        // 将修改过的字节码copy到dest,实现编译期间干预字节码
        FileUtils.copyDirectory(directoryInput.getFile(), dest);
    }

    /** 处理相应文件 */
    private void processingTheCorrespondingFile(File file) throws IOException {
        // 获取当前文件名称
        String fileName = file.getName();
        // 判断当前文件是否符合要求
        if (checkClassFile(fileName)) {
            // 打印当前符合条件的文件名称
            System.out.println("符合条件的类:" + fileName);
            // 准备待分析的class,进行ASM处理
            FileInputStream fis = new FileInputStream(file);
            // 对class文件进行读取与解析
            ClassReader classReader = new ClassReader(fis);
            // 对class文件的写入
            ClassWriter classWriter = new ClassWriter(classReader, ClassWriter.COMPUTE_MAXS);
            // 访问class文件相应的内容,解析到某一个结构就会通知到ClassVisitor的相应方法
            ClassVisitor classVisitor = new LifecycleClassVisitor(classWriter);
            // 依次调用 ClassVisitor接口的各个方法
            classReader.accept(classVisitor, ClassReader.EXPAND_FRAMES);
            // toByteArray方法会将最终修改的字节码以 byte 数组形式返回。
            byte[] bytes = classWriter.toByteArray();
            // 通过文件流写入方式覆盖掉原先的内容,实现class文件的改写。
//                FileOutputStream outputStream = new FileOutputStream( file.parentFile.absolutePath + File.separator + fileName)
            // 这个地址在javac目录下
            FileOutputStream outputStream = new FileOutputStream(file.getPath());
            // 写入流
            outputStream.write(bytes);
            // 关闭流
            outputStream.close();
        }
    }

    /** 遍历查找问题 */
    private void traverseToFindFiles(File dir) {
        // 获取所有目录
        File[] files = dir.listFiles();
        // 遍历所有目录节点
        for (File file : files) {
            // 判断是否为目录
            if (file.isDirectory()) {
                // 若是目录,则递归该目录下的文件
                traverseToFindFiles(file);
            }
            // 判断是否为文件
            else if (file.isFile()) {
                // 若是文件,载入集合
                mTemporaryFiles.add(file);
            }
        }
    }

    /** 检查class文件是否符合条件 */
    private boolean checkClassFile(String name) {
        return name.endsWith("Activity.class");
    }
}
4. 进行代码插入(插桩)

LifecycleClassVisitor

public class LifecycleClassVisitor extends ClassVisitor {

    private String className;

    public LifecycleClassVisitor(ClassVisitor cv) {
        /**
         * 参数1:ASM API版本,源码规定只能为4,5,6,7
         * 参数2:ClassVisitor 不能为 null
         */
        super(Opcodes.ASM7, 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);
        System.out.println("1 ===========================================================");
        System.out.println("name:" + name + " superName:" + superName + " signature:" + signature + " interfaces:" + interfaces);

        this.className = name;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
        System.out.println("2 ===========================================================");
        System.out.println("name:" + name + " desc:" + desc + " signature:" + signature + " exceptions:" + exceptions);
        MethodVisitor mv = cv.visitMethod(access, name, desc, signature, exceptions);
        // name为方法名,desc为描述(签名), 处理方法
        return new LifecycleMethodVisitor(mv, className, name);
    }

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

LifecycleMethodVisitor

public class LifecycleMethodVisitor extends MethodVisitor {

    /** 当前类名称 */
    private String className;
    /** 当前方法名称 */
    private String methodName;

    /** 当前是否为注解方法 */
    private boolean mInject;

    public LifecycleMethodVisitor(MethodVisitor methodVisitor, String className, String methodName) {
        super(Opcodes.ASM6, methodVisitor);
        this.className = className;
        this.methodName = methodName;
    }

    @Override
    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
        // 判断是否为指定注解类
//        if (Type.getDescriptor(InjectTimeStatistics.class).equals(desc)) {
//            System.out.println(desc);
//            mInject = true;
//        }
        // 也可以判断某个系列的注解
        if (desc.contains("InjectTimeStatistics")) {
            mInject = true;
        }
        return super.visitAnnotation(desc, visible);
    }

    /** 方法执行前插入 */
    @Override
    public void visitCode() {
        super.visitCode();
        if (mInject) {
            mv.visitLdcInsn(className + " -> TAG");
            mv.visitLdcInsn("\u5f00\u59cb\u65f6\u95f4:" + System.currentTimeMillis());
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
            mv.visitInsn(Opcodes.POP);
        }
    }

    /** 方法执行后插入 */
    @Override
    public void visitInsn(int opcode) {
        if (opcode == Opcodes.RETURN && mInject) {
            mv.visitLdcInsn(className + " -> TAG");
            mv.visitLdcInsn("\u7ed3\u675f\u65f6\u95f4:" + System.currentTimeMillis());
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "android/util/Log", "e", "(Ljava/lang/String;Ljava/lang/String;)I", false);
            mv.visitInsn(Opcodes.POP);
        }
        super.visitInsn(opcode);

    }

    @Override
    public void visitEnd() {
        super.visitEnd();
    }
}
5. 然后Rebuild Project,这个时候可以在build日志中看到输出的日志
6. 最后在build目录下找Javac目录查看已经插桩好的类文件

注解的使用(二):插桩,编译后处理筛选_jar_02

注:这里没有进行时间字段的定义,随便插入的时间,忽略就可以了!

点击下载 Demo

总结

1、ASM框架入门并不难,但是也不简单,对基础要求比较高,至少要掌握APK打包流程、自定义Gradle插件、Transform API以及AOP思想

2、使用感受

缺点:如果用过其它AOP框架,比如AspectJ,再来用ASM,会感觉到很难受、不好用,因为太复杂了,编写一个ASM工程对代码量怕是其它aop框架的几倍。原因:它是直接操作字节码指令的,这可是直接和JVM虚拟机打交道的底层内容,能不难吗?

优点:足够强大,几乎所有的CRUD操作都可以完成。由于是直接操作字节码,所以在效率上会比其它框架更高,注意:性能上没什么影响,因为是在编译期完成的。很多上层框架是用ASM作为底层技术的,比如Groovy、cglib等


标签:文件,字节,插桩,public,注解,后处理,class,ASM,String
From: https://blog.51cto.com/u_16163480/6511267

相关文章

  • 注解的使用(一):APT,编译时注解处理器
    关于编译时注解(APT)由浅入深有三部分,分别是:自定义注解处理器:例如ButterKnife、Room根据注解生成新的类;利用JcTree在编译时修改代码:像Lombok自动往类中新增getter/setter方法、往方法中插入代码行等;自定义Gradle插件在编译时修改代码:例如一些代码插桩框架,以及我司一些应......
  • EventBus 源码分析 - 注解 + 反射
    EventBus源码解析随着LiveData和KotlinFlow的出现,EventBus已经慢慢过时了。不过EventBus源码的设计思想以及实现原理还是值得我们去学习的。getDefault()方法EventBus().getDefault().register(this)首先EventBus的创建用到了DCL单例模式,源码如下:publicclassEventB......
  • 注解
    用于类上的注解@Accessors一般用chain=true,当该值为true时,调用setter方法时会返回当前的对象,方便采取链式编程的方法进行代码编写列如:CatSetName(123).setAge(20).setId();fluent属性为true时,对应的getter方法前没有get,setter方法前没有setprefix属性可以忽略前缀lombok注解......
  • Java 注解
    一、Java注解(Annotation)简介从Java5版本之后可以在源代码中嵌入一些补充信息,这种补充信息称为注解(Annotation),是Java平台中非常重要的一部分。注解都是@符号开头的,例如:在学习方法重写时使用过的@Override注解。同Class和Interface一样,注解也属于一种类型。Annotation......
  • springboot中自定义注解在service方法中,aop失效
    问题描述写了个自定义注解,但是该注解只会出现在serviece层中的方法中。启动发现aop未拦截到问题原因:调用service中的xx()方法时,Spring的动态代理帮我们动态生成了一个代理的对象,暂且叫他$XxxxService。所以调用xx()方法实际上是代理对象$XxxxService调用的。但是在xx()方法内调用同......
  • 3. @RequestMapping注解
    1.@RequestMapping注解的功能‍@RequestMapping注解的作用就是将请求和处理请求的控制器方法关联起来,建立映射关系。‍SpringMVC接收到指定的请求,就会来找到在映射关系中对应的控制器方法来处理这个请求‍2.@RequestMapping注解的位置‍@RequestMapping标识一个......
  • Java官方笔记10注解
    注解注解的作用:Informationforthecompiler—Annotationscanbeusedbythecompilertodetecterrorsorsuppresswarnings.Compile-timeanddeployment-timeprocessing—Softwaretoolscanprocessannotationinformationtogeneratecode,XMLfiles,ands......
  • @Import注解
    1.前言 在之前关于springBoot自动装配一文里,曾经写过里面有一个很关键的注解,就是@Import,它让springBoot可以将spring.factories中的各组件的配置类加载到spring容器中,那么今天来讲下关于这个注解的一些了解。2.import注解讲解2.1@Import注解的作用向spring容器中添加实......
  • 注解 annotation
    内置注解@Override:重写@Deprecated:不推荐使用的@SupperessWarnings("all"):镇压警告元注解用于负责注解其他注解@Target:解释被描述的注解的使用范围@Retention:解释需要在什么级别保存被描述的注解信息(SOURCE<CLASS<RUNTIME)@Document:解释被描述的注解被......
  • Spring注解开发
    注解开发介绍:注解开发是spring的强项,实际开发过程中更多使用的是注解注入而非bean标签注入xml和注解开发的对比:xml可以适用任何场景,结构清晰,维护方便注解不是自己提供的类使用不了,开发简单方便建议使用xml和注解整合开发xml管理Bean注解完成属性注入使用过程中,可以......