0%

Java筑基-:(01)Retrofit中的注解反射与动态代理

注解的作用: 用于标记,接口写成 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//利用 APT 新增一个类
public class APTTest extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
String code = "public class A {\n" +
"}";
Filer filer = processingEnv.getFiler();
OutputStream outputStream = null;
try {
//为什么用 JavaFileObject?因为它能默认将生成文件放到 build 目录下,无需你手动去加路径
JavaFileObject sourceFile = filer.createClassFile("A");
outputStream = sourceFile.openOutputStream();
//将代码写入文件
outputStream.write(code.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (outputStream != null) {
outputStream.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
}

javac 的执行流程如下:

javac的执行流程

发现注解之后,执行注解处理器,因为你在注解处理器里面可能会做改变(或者能新增了Java文件),因此能看到注解处理之后会有一条线回去,重新开始处理下。所以,注解处理器中的 process 方法可能会执行多次(因为新生成的类中可能还包括注解,所以可能是非常多的次数)。

因为 process 方法会执行多次,所以我们必须要判断注解过程是否执行完了,如果已经执行完了,就不要再去生成文件 A 了,可以通过如下方法判断过程执行完:

1
2
3
annotations.isEmpty()

roundEnv.processingOver()

三、字节码插桩技术

为什么字节码插桩?因为你还是 Java 代码的时候,可能没有这种条件去做这个事情。比如,组件 A 中需要 new 出 B 组件中的 一个类,由于大家都是互相独立的,没有引用,但是 字节码中就不一样了,所有的类都变成了 class 了,肯定有引用的。典型的如 ARouter 中的 路由表的实现。

还有个典型的例子是 腾讯的 Matrix 性能监控的实现

字节码插桩也能用来实现 AOP ,比如 360 的 APM ,这样就能实现在代码中的判断。

字节码插桩的本质: .class 是个文件,它有自己的格式,可以作为读入,按照一定的格式修改,再保存就可以了

四、反射

我们能够通过反射修改 final 类型的成员的值吗?答案是肯定的,但是肯定有人经历过下面这种情景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ReflectTest {

public final int a = 1;

public int getA(){
return a;
}

public static void main(String[] args) throws Exception {
ReflectTest reflectTest = new ReflectTest();
Field a = ReflectTest.class.getDeclaredField("a");
a.setAccessible(true);
a.set(reflectTest, 2);

System.out.println("a = " + reflectTest.a);
}
}

通过上面的代码反射修改 a 的值为 2,但是最终 println 打印出来的却是 1 ,这是没有修改?其实不是,这是因为 Java 编译过程的优化导致的,这里是内联,我们看下它编译成的字节码,反编译过来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ReflectTest {
public final int a = 1;

public ReflectTest() {
}

public int getA() {
return 1;
}

public static void main(String[] args) throws Exception {
ReflectTest reflectTest = new ReflectTest();
Field a = ReflectTest.class.getDeclaredField("a");
a.setAccessible(true);
a.set(reflectTest, 2);
PrintStream var10000 = System.out;
Objects.requireNonNull(reflectTest);
var10000.println("a = " + 1);
}
}

看到代码最后一行,我们的 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
2
3
4
@Click(R.id.button)
public void click(View view){

}
谢谢你的鼓励