注解深入浅出之-注解的使用(二)

更多andorid高级架构进阶视频免费分享学习请点击:https://space.bilibili.com/474380680

一、APT

1.什么是APT?
APT即为Annotation Processing Tool,它是javac的一个工具,中文意思为编译时注解处理器。APT可以用来在编译时扫描和处理注解。通过APT可以获取到注解和被注解对象的相关信息,在拿到这些信息后我们可以根据需求来自动的生成一些代码,省去了手动编写。注意,获取注解及生成代码都是在代码编译时候完成的,相比反射在运行时处理注解大大提高了程序性能。APT的核心是AbstractProcessor类,关于AbstractProcessor类后面会做详细说明。
2.哪里用到了APT?
APT技术被广泛的运用在Java框架中,包括Android项以及Java后台项目,除了上面我们提到的ButterKnife之外,像EventBus 、Dagger2以及阿里的ARouter路由框架等都运用到APT技术,因此要想了解以、探究这些第三方框架的实现原理,APT就是我们必须要掌握的。

3.如何在Android Studio中构建一个APT项目?
APT项目需要由至少两个Java Library模块组成,不知道什么是Java Library?没关系,手把手来叫你如何创建一个Java Library。

首先,新建一个Android项目,然后File–>New–>New Module,打开如上图所示的面板,选择Java Library即可。刚才说到一个APT项目至少应该由两个Java Library模块。那么这两个模块分别是什么作用呢?
1.首先需要一个Annotation模块,这个用来存放自定义的注解。

  1. 另外需要一个Compiler模块,这个模块依赖Annotation模块。
    3.项目的App模块和其它的业务模块都需要依赖Annotation模块,同时需要通过annotationProcessor依赖Compiler模块。
    app模块的gradle中依赖关系如下:

implementation project(':annotation')
annotationProcessor project(':factory-compiler')

APT项目的模块的结构图如下所示:

为什么要强调上述两个模块一定要是Java Library?如果创建Android Library模块你会发现不能找到AbstractProcessor这个类,这是因为Android平台是基于OpenJDK的,而OpenJDK中不包含APT的相关代码。因此,在使用APT时,必须在Java Library中进行。
<meta charset="utf-8">

二、插桩

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

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

15590149-299a37c737fce1fb
image
15590149-f2aba8e5b91ef2d5
image

我们需要查看方法执行耗时,如果每一个方法都需要自己手动去加入这些内容,当不需要时也需要一个个删去相应的代码。一个、两个方法还好,如果有10个、20个得多麻烦!所以可以利用注解来标记需要插桩的方法,结合编译后操作字节码来帮助我们自动插入,当不需要时关掉插桩即可。这种AOP思想让我们只需要关注插桩代码本身。

2、字节码操作框架

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

我们非常熟悉的JSON格式数据是基于文本的,我们只需要知道它的规则就能够轻松的生成、修改JSON数据。同样的Class字节码也有其自己的规则(格式)。操作JSON可以借助GSON来非常方便的生成、修改JSON数据。而字节码Class,同样可以借助Javassist/ASM来实现对其修改。

15590149-a5485a1edc685cd1
image

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

3、ASM的使用*

由于 ASM具有相对于 Javassist更好的性能以及更高的灵活行,我们这篇文章以使用ASM为主。在真正利用到Android中之前,我们可以先在 Java程序中完成对字节码的修改测试。

3.1、在AS中引入ASM

ASM可以直接从 jcenter()仓库中引入,所以我们可以进入:https://bintray.com/进行搜索

15590149-990de45254705f3e
image

点击图中标注的工件进入,可以看到最新的正式版本为:7.1。

15590149-f1054b25767e9044
image

因此,我们可以在AS中加入:

15590149-f671bbb5ee6514d7
image

同时,需要注意的是:我们使用 testImplementation引入,这表示我们只能在Java的单元测试中使用这个框架,对我们Android中的依赖关系没有任何影响。

AS中使用gradle的Android工程会自动创建Java单元测试与Android单元测试。测试代码分别在test与androidTest。

3.2、准备待插桩Class

在 test/java下面创建一个Java类:

public class InjectTest {
       public static void main(String[] args) {
    }
}

由于我们操作的是字节码插桩,所以可以进入 test/java下面使用 javac对这个类进行编译生成对应的class文件。

javac InjectTest.java

3.3、执行插桩

因为 main方法中没有任何输出代码,我们输入命令:javaInjectTest执行这个Class不会有任何输出。那么我们接下来利用 ASM,向 main方法中插入一开始图中的记录函数执行时间的日志输出。

在单元测试中写入测试方法

/**
* 1、准备待分析的class
*/
FileInputStream fis = new  FileInputStream
("xxxxx/test/java/InjectTest.class");
/**
* 2、执行分析与插桩
*/
//class字节码的读取与分析引擎
ClassReader cr = new  ClassReader(fis);
// 写出器 COMPUTE_FRAMES 自动计算所有的内容,后续操作更简单
ClassWriter cw = new  ClassWriter(ClassWriter.COMPUTE_FRAMES);
//分析,处理结果写入cw EXPAND_FRAMES:栈图以扩展格式进行访问
cr.accept(new  ClassAdapterVisitor(cw), ClassReader.EXPAND_FRAMES);
/**
* 3、获得结果并输出
*/
byte[] newClassBytes = cw.toByteArray();
File file = new  File("xxx/test/java2/");
file.mkdirs();
FileOutputStream fos = new  FileOutputStream
("xxx/test/java2/InjectTest.class");
fos.write(newClassBytes);
fos.close();

关于ASM框架本身的设计,我们这里先不讨论。上面的代码会获取上一步生成的class,然后由ASM执行完插桩之后,将结果输出到 test/java2目录下。其中关键点就在于第2步中,如何进行插桩。

把class数据交给 ClassReader,然后进行分析,类似于XML解析,分析结果会以事件驱动的形式告知给accept的第一个参数 ClassAdapterVisitor

public  class  ClassAdapterVisitor  extends  ClassVisitor {
    public  ClassAdapterVisitor(ClassVisitor cv) {
        super(Opcodes.ASM7, cv);
    }
    @Override
    public  MethodVisitor visitMethod(int access, String name, String desc, String signature,
    String[] exceptions) {
        System.out.println("方法:" + name + " 签名:" + desc);
        MethodVisitor mv = super.visitMethod(access, name, desc, signature,
        exceptions);
        return  new  MethodAdapterVisitor(api,mv, access, name, desc);
    }
}

分析结果通过 ClassAdapterVisitor获得,一个类中会存在方法、注解、属性等,因此 ClassReader会将调用 ClassAdapterVisitor中对应的 visitMethodvisitAnnotationvisitField这些 visitXX方法。

我们的目的是进行函数插桩,因此重写 visitMethod方法,在这个方法中我们返回一个 MethodVisitor方法分析器对象。一个方法的参数、注解以及方法体需要在 MethodVisitor中进行分析与处理。

package com.enjoy.asminject.example;
import com.enjoy.asminject.ASMTest;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.commons.AdviceAdapter;
import org.objectweb.asm.commons.Method;
/**
* AdviceAdapter: 子类
* 对methodVisitor进行了扩展, 能让我们更加轻松的进行方法分析
*/
public  class  MethodAdapterVisitor  extends  AdviceAdapter {
    private  Boolean inject;
    protected  MethodAdapterVisitor(int api, MethodVisitor methodVisitor, int access, String name, String descriptor) {
        super(api, methodVisitor, access, name, descriptor);
    }
    /**
* 分析方法上面的注解
* 在这里干嘛???
* <p>
* 判断当前这个方法是不是使用了injecttime,如果使用了,我们就需要对这个方法插桩
* 没使用,就不管了。
*
* @param desc
* @param visible
* @return
*/
    @Override
    public  AnnotationVisitor visitAnnotation(String desc, Boolean visible) {
        if (Type.getDescriptor(ASMTest.class).equals(desc)) {
            System.out.println(desc);
            inject = true;
        }
        return  super.visitAnnotation(desc, visible);
    }
    private  int start;
    @Override
    protected  void onMethodEnter() {
        super.onMethodEnter();
        if (inject) {
            //执行完了怎么办?记录到本地变量中
            invokeStatic(Type.getType("Ljava/lang/System;"),
            new  Method("currentTimeMillis", "()J"));
            start = newLocal(Type.LONG_TYPE);
            //创建本地 LONG类型变量
            //记录 方法执行结果给创建的本地变量
            storeLocal(start);
        }
    }
    @Override
    protected  void onMethodExit(int opcode) {
        super.onMethodExit(opcode);
        if (inject){
            invokeStatic(Type.getType("Ljava/lang/System;"),
            new  Method("currentTimeMillis", "()J"));
            int end = newLocal(Type.LONG_TYPE);
            storeLocal(end);
            getStatic(Type.getType("Ljava/lang/System;"),"out",Type.getType("Ljava/io" +
            "/PrintStream;"));
            //分配内存 并dup压入栈顶让下面的INVOKESPECIAL 知道执行谁的构造方法创建StringBuilder
            newInstance(Type.getType("Ljava/lang/StringBuilder;"));
            dup();
            invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"),new  Method("<init>","()V"));
            visitLdcInsn("execute:");
            invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new  Method("append","(Ljava/lang/String;)Ljava/lang/StringBuilder;"));
            //减法
            loadLocal(end);
            loadLocal(start);
            math(SUB,Type.LONG_TYPE);
            invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new  Method("append","(J)Ljava/lang/StringBuilder;"));
            invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),new  Method("toString","()Ljava/lang/String;"));
            invokeVirtual(Type.getType("Ljava/io/PrintStream;"),new  Method("println","(Ljava/lang/String;)V"));
        }
    }
}

MethodAdapterVisitor继承自 AdviceAdapter,其实就是 MethodVisitor 的子类, AdviceAdapter封装了指令插入方法,更为直观与简单。

上述代码中 onMethodEnter进入一个方法时候回调,因此在这个方法中插入指令就是在整个方法最开始加入一些代码。我们需要在这个方法中插入 longs=System.currentTimeMillis();。在 onMethodExit中即方法最后插入输出代码。

@Override
protected  void onMethodEnter() {
    super.onMethodEnter();
    if (inject) {
        //执行完了怎么办?记录到本地变量中
        invokeStatic(Type.getType("Ljava/lang/System;"),
        new  Method("currentTimeMillis", "()J"));
        start = newLocal(Type.LONG_TYPE);
        //创建本地 LONG类型变量
        //记录 方法执行结果给创建的本地变量
        storeLocal(start);
    }
}

这里面的代码怎么写?其实就是 longs=System.currentTimeMillis();这句代码的相对的指令。我们可以先写一份代码

void test(){
    //插入的代码
    long s = System.currentTimeMillis();
    /**
*  方法实现代码....
*/
    //插入的代码
    long e = System.currentTimeMillis();
    System.out.println("execute:"+(e-s)+" ms.");
}

然后使用 javac编译成Class再使用 javap-c查看字节码指令。也可以借助插件来查看,就不需要我们手动执行各种命令。

15590149-e9855d4aeb3e14b1
image

安装完成之后,可以在需要插桩的类源码中点击右键:

15590149-595b471115c44473
image

点击ASM Bytecode Viewer之后会弹出

15590149-d23024fb89b7a9f7
image

所以第20行代码: longs=System.currentTimeMillis();会包含两个指令: INVOKESTATICLSTORE

再回到 onMethodEnter方法中

@Override
protected  void onMethodEnter() {
    super.onMethodEnter();
    if (inject) {
        //invokeStatic指令,调用静态方法
        invokeStatic(Type.getType("Ljava/lang/System;"),
        new  Method("currentTimeMillis", "()J"));
        //创建本地 LONG类型变量
        start = newLocal(Type.LONG_TYPE);
        //store指令 将方法执行结果从操作数栈存储到局部变量
        storeLocal(start);
    }
}

onMethodExit也同样根据指令去编写代码即可。最终执行完插桩之后,我们就可以获得修改后的class数据。

三、反射

1. 什么是反射
反射:是一种机制,利用该机制可以在程序运行过程中对类进行解剖并操作类中的方法,属性,构造方法等成员。

是另外一种调用构造方法、普通方法、属性的方式。

对比普通方式:

功能更强

但是更麻烦

反射操作的统一步骤:

获取Class字节码对象

要操作什么,就获取什么:getxxx(), getDeclaredXXX()

要操作构造方法,就获取Constructor对象

要操作普通方法,就获取Method对象

要操作属性字段,就获取Field对象

如果操作是private类型,就提前设置允许暴力反射:setAccessible(true)

2、反射调用

反射调用Constructor:newInstance()

反射调用Method:invoke()

反射调用Filed: set(), get()

更多andorid高级架构进阶视频免费分享学习请点击:https://space.bilibili.com/474380680
参考:https://www.jianshu.com/p/cce778d149bc
https://blog.csdn.net/qq_20521573/article/details/82321755

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值