注解的作用: 用于标记,接口写成 interface ,注解 的写法是 @interface 。
一、元注解
元注解,我们用得比较多的是下面 2 种:
@Target : 针对哪些地方来作用这个注解,有 TYPE (作用在类、接口、枚举,甚至注解上等)、METHOD(方法)、FIELD (属性)、PARAMETER(参数)。 Target 注解的value 是个数组,可以多个的
@Retention : 表示可以将注解信息保存到什么层次。有 SOURCE(源码)、CLASS(.class文件)、RUNTIME(虚拟机级别)
Retention 中三种级别的比较:
级别 | 典型技术 | 使用场景 | 例子 |
---|---|---|---|
SOURCE | APT(annotion processor tool:注解处理工具) | 编译期获取注解与其成员信息,一般用于生成辅助类 | InDef、StringDef,参数只接受指定的几种值 |
CLASS | 字节码增强、插桩 | 编译出class文件后,对class修改 | |
RUNTIME | 反射 | 运行期间,反射获取注解与其元素 |
注意:对于 Android 而言,打包 dex 的时候,CLASS 级别的注解都会被抛弃掉,但是 RUNTIME 级别的会保留
IntDef 的实现原理:传参非指定的时,会报红,看着是报错,但是是不影响运行的!它的原理就是 lint 检测
二、自定义 APT (annotion processor tool)
我们可以自定义注解处理器,自定义的类继承 AbstractProcessor ,然后注册。
自定义的注解处理器不会被编译到代码中,只是编译的时候使用。这里没有详细写明怎么去自定义,如果后续用到了,可以再去看视频
1 | //利用 APT 新增一个类 |
javac 的执行流程如下:
发现注解之后,执行注解处理器,因为你在注解处理器里面可能会做改变(或者能新增了Java文件),因此能看到注解处理之后会有一条线回去,重新开始处理下。所以,注解处理器中的 process 方法可能会执行多次(因为新生成的类中可能还包括注解,所以可能是非常多的次数)。
因为 process 方法会执行多次,所以我们必须要判断注解过程是否执行完了,如果已经执行完了,就不要再去生成文件 A 了,可以通过如下方法判断过程执行完:
1 | annotations.isEmpty() |
三、字节码插桩技术
为什么字节码插桩?因为你还是 Java 代码的时候,可能没有这种条件去做这个事情。比如,组件 A 中需要 new 出 B 组件中的 一个类,由于大家都是互相独立的,没有引用,但是 字节码中就不一样了,所有的类都变成了 class 了,肯定有引用的。典型的如 ARouter 中的 路由表的实现。
还有个典型的例子是 腾讯的 Matrix 性能监控的实现
字节码插桩也能用来实现 AOP ,比如 360 的 APM ,这样就能实现在代码中的判断。
字节码插桩的本质: .class 是个文件,它有自己的格式,可以作为读入,按照一定的格式修改,再保存就可以了
四、反射
我们能够通过反射修改 final 类型的成员的值吗?答案是肯定的,但是肯定有人经历过下面这种情景:
1 | public class ReflectTest { |
通过上面的代码反射修改 a 的值为 2,但是最终 println 打印出来的却是 1 ,这是没有修改?其实不是,这是因为 Java 编译过程的优化导致的,这里是内联,我们看下它编译成的字节码,反编译过来:
1 | public class ReflectTest { |
看到代码最后一行,我们的 reflectTest.a 已经直接 变成 1 了,并且注意看 getA() 方法 ,它里面return a 也直接变为 return 1 。所以,我们可以知道,其实 a 的值是已经改了,但是通过上述的代码看不出来。
那么问题来了,如果我想获取到这个修改后的 a 值怎么办? 答案还是通过反射获取,将最后一行的打印改成这样就行,就能正常输出修改后的 2 值了:
1 | System.out.println("a = " + a.getInt(reflectTest)); |
4.1 反射调用方法为什么耗时
比如通过对象普通调用方法,类似 object.method() , 字节码中用 iconst_1 指令即可完成,不需要做额外事情,但是对于反射获取而言
但是反射,我们调用的的是 Object.invoke (Object obj, Object…args) 能看出来,参数是 变长 的了,并且还都是 Object ,意味着你普通方法是 int 型 的话,这里需要装箱成 Integer类型,再包装成 Object 数组,在执行的时候又会把数组拆开,并且拆箱为基本数据类型。
从源码可知,反射需要遍历所有的方法,匹配方法名和参数,然后才能得到正确的目标方法
反射时需要检查方法的可见性,以及参数的匹配性
反射时,编译期无法对动态调用的代码做优化,比如 内联
比如,上面我们更改 a 的值为 2 ,通过反射时就能获取这个值,说明是没有做优化的
当然,现在硬件性能已经非常强了,反射能带来的影响还是比较小的,。
五、动态代理
动态代理的原理是,会自动给你生成代码,相当于修改字节码。自己去看下编译期给自动生成的那个 Proxy 类。
动态代理的原理还不太清楚,下次来需要着重梳理
作业
注解+反射+ 动态代理实现 @Click 方法,动态代理为了开闭原则,如下:
1 |
|