反射介绍
正常情况下,我们知晓我们要操作的类和对象是什么,可以直接操作这些对象中的变量和方法,比如一个User类:
代码语言:txt复制User user=new User();
代码语言:txt复制user.setName("Bob");
但是有的场景,我们无法正常去操作:
- 只知道类路径,无法直接实例化的对象。
- 无法直接操作某个对象的变量和方法,比如私有方法,私有变量。
- 需要hook系统逻辑,比如修改某个实例的参数。
等等情况。
所以我们就需要一种机制能让我们去操作任意的类和对象。
这种机制,就是反射
。简单的说,反射就是:
对于任意一个类
,都能够知道这个类的所有属性和方法;
对于任意一个对象
,都能够调用它的任意方法和属性。
常用API举例
先设定一个User类:
代码语言:txt复制package com.example.testapplication.reflection;
代码语言:txt复制public class User {
代码语言:txt复制 private int age;
代码语言:txt复制 public String name;
代码语言:txt复制 public User() {
代码语言:txt复制 System.out.println("调用了User()");
代码语言:txt复制 }
代码语言:txt复制 private User(int age, String name) {
代码语言:txt复制 this.name = name;
代码语言:txt复制 this.age = age;
代码语言:txt复制 System.out.println("调用了User(age,name)" "__age:" age "__name:" name);
代码语言:txt复制 }
代码语言:txt复制 public User(String name) {
代码语言:txt复制 this.name = name;
代码语言:txt复制 System.out.println("调用了User(name)" "__name:" name);
代码语言:txt复制 }
代码语言:txt复制 private String getName() {
代码语言:txt复制 System.out.println("调用了getName()");
代码语言:txt复制 return this.name;
代码语言:txt复制 }
代码语言:txt复制 private String setName(String name) {
代码语言:txt复制 this.name = name;
代码语言:txt复制 System.out.println("调用了setName(name)__" name);
代码语言:txt复制 return this.name;
代码语言:txt复制 }
代码语言:txt复制 public int getAge() {
代码语言:txt复制 System.out.println("调用了getAge()");
代码语言:txt复制 return this.age;
代码语言:txt复制 }
代码语言:txt复制}
获取Class对象
主要有三种方法获取Class对象
:
- 根据类路径获取类对象
- 直接获取
- 实例对象的getclass方法
//1、根据类路径获取类对象
代码语言:txt复制try {
代码语言:txt复制 Class clz = Class.forName("com.example.testapplication.reflection.User");
代码语言:txt复制} catch (ClassNotFoundException e) {
代码语言:txt复制 e.printStackTrace();
代码语言:txt复制}
代码语言:txt复制//2、直接获取
代码语言:txt复制Class clz = User.class;
代码语言:txt复制//3、对象的getclass方法
代码语言:txt复制Class clz = new User().getClass();
获取类的构造方法
1、获取类所有构造方法
代码语言:txt复制Class clz = User.class;
代码语言:txt复制//获取所有构造函数(不包括私有构造方法)
代码语言:txt复制Constructor[] constructors1 = clz.getConstructors();
代码语言:txt复制//获取所有构造函数(包括私有构造方法)
代码语言:txt复制Constructor[] constructors2 = clz.getDeclaredConstructors();
2、获取类的单个构造方法
代码语言:txt复制 try {
代码语言:txt复制 //获取无参构造函数
代码语言:txt复制 Constructor constructor1 = clz.getConstructor();
代码语言:txt复制 //获取参数为String的构造函数
代码语言:txt复制 Constructor constructor2 =clz.getConstructor(String.class);
代码语言:txt复制 //获取参数为int,String的构造函数
代码语言:txt复制 Class[] params = {int.class,String.class};
代码语言:txt复制 Constructor constructor3 =clz.getDeclaredConstructor(params);
代码语言:txt复制 } catch (NoSuchMethodException e) {
代码语言:txt复制 e.printStackTrace();
代码语言:txt复制 }
需要注意的是,User(int age, String name)
为私有构造方法,所以需要使用getDeclaredConstructor
获取。
调用类的构造方法生成实例对象
1、调用Class对象的newInstance
方法
这个方法只能调用无参构造函数,也就是Class
对象的newInstance
方法不能传入参数。
Object user = clz.newInstance();
2、调用Constructor
对象的newInstance
方法
Class[] params = {int.class,String.class};
代码语言:txt复制Constructor constructor3 =clz.getDeclaredConstructor(params);
代码语言:txt复制constructor3.setAccessible(true);
代码语言:txt复制constructor3.newInstance(22,"Bob");
这里要注意下,虽然getDeclaredConstructor
能获取私有构造方法,但是如果要调用这个私有方法,需要设置setAccessible(true)
方法,否则会报错:
can not access a member of class com.example.testapplication.reflection.User with modifiers "private"
获取类的属性(包括私有属性)
代码语言:txt复制Class clz = User.class;
代码语言:txt复制Field field1 = clz.getField("name");
代码语言:txt复制Field field2 = clz.getDeclaredField("age");
同样的,getField
获取public
类变量,getDeclaredField
可以获取所有变量(包括私有变量属性)。
所以一般直接用getDeclaredField即可。
修改实例的属性
接上例,获取类的属性后,可以去修改类实例的对应属性,比如我们有个user
的实例对象,我们来修改它的name和age。
//修改name,name为public属性
代码语言:txt复制Class clz = User.class;
代码语言:txt复制Field field1 = clz.getField("name");
代码语言:txt复制field1.set(user,"xixi");
代码语言:txt复制//修改age,age为private属性
代码语言:txt复制Class clz = User.class;
代码语言:txt复制Field field2 = clz.getDeclaredField("age");
代码语言:txt复制field2.setAccessible(true);
代码语言:txt复制field2.set(user,123);
获取类的方法(包括私有方法)
代码语言:txt复制 //获取getName方法
代码语言:txt复制 Method method1 = clz.getDeclaredMethod("getName");
代码语言:txt复制 //获取setName方法,带参数
代码语言:txt复制 Method method2 = clz.getDeclaredMethod("setName", String.class);
代码语言:txt复制 //获取getage方法
代码语言:txt复制 Method method3 = clz.getMethod("getAge");
调用实例的方法
代码语言:txt复制method1.setAccessible(true);
代码语言:txt复制Object name = method1.invoke(user);
代码语言:txt复制method2.setAccessible(true);
代码语言:txt复制method2.invoke(user, "xixi");
代码语言:txt复制Object age = method3.invoke(user);
反射优缺点
虽然反射很好用,增加了程序的灵活性,但是也有他的缺点:
性能问题
。由于用到动态类型(运行时才检查类型),所以反射的效率比较低。但是对程序的影响比较小,除非对性能要求比较高。所以需要在两者之间平衡。不够安全
。由于可以执行一些私有的属性和方法,所以可能会带来安全问题。不易读写
。当然这一点也有解决方案,比如jOOR库,但是不适用于Android定义为final的字段。
Android中的应用
插件化(Hook)
Hook 技术又叫做钩子函数,在系统没有调用该函数之前,钩子程序就先捕获该消息,钩子函数先得到控制权,这时钩子函数既可以加工处理(改变)该函数的执行行为,还可以强制结束消息的传递。
在插件化中,我们需要找到可以hook的点,然后进行一些插件的工作,比如替换Activity,替换mH
等等。这其中就用到大量反射的知识,这里以替换mH为例:
// 获取到当前的ActivityThread对象
代码语言:txt复制Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
代码语言:txt复制Field currentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
代码语言:txt复制currentActivityThreadField.setAccessible(true);
代码语言:txt复制Object currentActivityThread = currentActivityThreadField.get(null);
代码语言:txt复制//获取这个对象的mH
代码语言:txt复制Field mHField = activityThreadClass.getDeclaredField("mH");
代码语言:txt复制mHField.setAccessible(true);
代码语言:txt复制Handler mH = (Handler) mHField.get(currentActivityThread);
代码语言:txt复制//替换mh为我们自己的HandlerCallback
代码语言:txt复制Field mCallBackField = Handler.class.getDeclaredField("mCallback");
代码语言:txt复制mCallBackField.setAccessible(true);
代码语言:txt复制mCallBackField.set(mH, new MyActivityThreadHandlerCallback(mH));
动态代理
动态代理的特点是不需要提前创建代理对象,而是利用反射机制
在运行时创建代理类,从而动态实现代理功能。
public class InvocationTest implements InvocationHandler {
代码语言:txt复制 // 代理对象(代理接口)
代码语言:txt复制 private Object subject;
代码语言:txt复制 public InvocationTest(Object subject) {
代码语言:txt复制 this.subject = subject;
代码语言:txt复制 }
代码语言:txt复制 @Override
代码语言:txt复制 public Object invoke(Object object, Method method, Object[] args)
代码语言:txt复制 throws Throwable {
代码语言:txt复制 //代理真实对象之前
代码语言:txt复制 Object obj = method.invoke(subject, args);
代码语言:txt复制 //代理真实对象之后
代码语言:txt复制 return obj;
代码语言:txt复制 }
代码语言:txt复制}
三方库(注解)
我们可以发现很多库都会用到注解,而获取注解的过程也会有反射的过程,比如获取Activity
中所有变量的注解:
public void getAnnotation(Activity activity){
代码语言:txt复制 Class clazz = activity.getClass();
代码语言:txt复制 //获得activity中的所有变量
代码语言:txt复制 Field[] fields = clazz.getDeclaredFields();
代码语言:txt复制 for (Field field : fields) {
代码语言:txt复制 field.setAccessible(true);
代码语言:txt复制 //获取变量上加的注解
代码语言:txt复制 MyAnnotation test = field.getAnnotation(MyAnnotation.class);
代码语言:txt复制 //...
代码语言:txt复制 }
代码语言:txt复制}
这种通过反射处理注解的方式称作运行时注解,也就是程序运行状态的时候才会去处理注解。
但是上文说过了,反射会在一定程度上影响到程序的性能,所以还有一种处理注解的方式:编译时注解。
所用到的注解处理工具是APT
。
APT是一种注解处理器,可以在编译时进行扫描和处理注解,然后生成java代码文件,这种方法对比反射就能比较小的影响到程序的运行性能。
这里就不说APT的使用了,下次会专门有章节提到~
反射可以修改final类型成员变量吗?
final我们应该都知道,修饰变量的时候代表是一个常量,不可修改。那利用反射能不能达到修改的效果呢?
我们先试着修改一个用final修饰的String
变量。
public class User {
代码语言:txt复制 private final String name = "Bob";
代码语言:txt复制 private final Student student = new Student();
代码语言:txt复制 public String getName() {
代码语言:txt复制 return name;
代码语言:txt复制 }
代码语言:txt复制 public Student getStudent() {
代码语言:txt复制 return student;
代码语言:txt复制 }
代码语言:txt复制}
代码语言:txt复制 User user = new User();
代码语言:txt复制 Class clz = User.class;
代码语言:txt复制 Field field1 = null;
代码语言:txt复制 try{
代码语言:txt复制 field1=clz.getDeclaredField("name");
代码语言:txt复制 field1.setAccessible(true);
代码语言:txt复制 field1.set(user,"xixi");
代码语言:txt复制 System.out.println(user.getName());
代码语言:txt复制 }catch(NoSuchFieldException e){
代码语言:txt复制 e.printStackTrace();
代码语言:txt复制 }catch(IllegalAccessException e){
代码语言:txt复制 e.printStackTrace();
代码语言:txt复制 }
打印出来的结果,还是Bob
,也就是没有修改到。
我们再修改下student
变量试试:
field1 = clz.getDeclaredField("student");
代码语言:txt复制field1.setAccessible(true);
代码语言:txt复制field1.set(user, new Student());
代码语言:txt复制打印:
代码语言:txt复制修改前com.example.studynote.reflection.Student@77459877
代码语言:txt复制修改后com.example.studynote.reflection.Student@72ea2f77
可以看到,对于正常的对象变量即使被final
修饰也是可以通过反射进行修改的。
这是为什么呢?为什么String
不能被修改,而普通的对象变量可以被修改呢?
先说结论,其实String
值也被修改了,只是我们无法通过这个对象获取到修改后的值。
这就涉及到JVM的内联优化
了:
内联函数,编译器将指定的函数体插入并取代每一处调用该函数的地方(上下文),从而节省了每次调用函数带来的额外时间开支。
简单的说,就是JVM在处理代码的时候会帮我们优化代码逻辑,比如上述的final变量
,已知final
修饰后不会被修改,所以获取这个变量的时候就直接帮你在编译阶段就给赋值了。
所以上述的getName
方法经过JVM编译内联优化后会变成:
public String getName() {
代码语言:txt复制 return "Bob";
代码语言:txt复制 }
所以无论怎么修改,都获取不到修改后的值。
有的朋友可能提出直接获取name呢?比如这样:
代码语言:txt复制//修改为public
代码语言:txt复制public final String name = "Bob";
代码语言:txt复制//反射修改后,打印user.name
代码语言:txt复制field1=clz.getDeclaredField("name");
代码语言:txt复制field1.setAccessible(true);
代码语言:txt复制field1.set(user,"xixi");
代码语言:txt复制System.out.println(user.name);
不好意思,还是打印出来Bob。这是因为System.out.println(user.name)
这一句在经过编译后,会被写成:
System.out.println(user.name)
代码语言:txt复制//经过内联优化
代码语言:txt复制System.out.println("Bob")
所以:
反射是可以修改final变量的,但是如果是基本数据类型或者String类型的时候,无法通过对象获取修改后的值,因为JVM对其进行了内联优化。
那有没有办法获取修改后的值呢?
有,可以通过反射中的Field.get(Object obj)
方法获取:
//获取field对应的变量在user对象中的值
代码语言:txt复制System.out.println("修改后" field.get(user));
反射获取static静态变量
说完了final,再说说static
,怎么修改static修饰的变量呢?
我们知道,静态变量是在类的实例化之前就进行了初始化(类的初始化阶段)
,所以静态变量是跟着类本身走的,跟具体的对象无关,所以我们获取变量就不需要传入对象,直接传入null即可:
public class User {
代码语言:txt复制 public static String name;
代码语言:txt复制}
代码语言:txt复制field2 = clz.getDeclaredField("name");
代码语言:txt复制field2.setAccessible(true);
代码语言:txt复制//获取静态变量
代码语言:txt复制Object getname=field2.get(null);
代码语言:txt复制System.out.println("修改前" getname);
代码语言:txt复制//修改静态变量
代码语言:txt复制field2.set(null, "xixi");
代码语言:txt复制System.out.println("修改后" User.name);
如上述代码:
Field.get(null)
可以获取静态变量。Field.set(null,object)
可以修改静态变量。
怎么提升反射效率
- 1、缓存重复用到的对象
利用缓存,其实我不说大家也都知道,在平时项目中用到多次的对象也会进行缓存,谁也不会多次去创建。
但是,这一点在反射中尤为重要,比如Class.forName
方法,我们做个测试:
long startTime = System.currentTimeMillis();
代码语言:txt复制 Class clz = Class.forName("com.example.studynote.reflection.User");
代码语言:txt复制 User user;
代码语言:txt复制 int i = 0;
代码语言:txt复制 while (i < 1000000) {
代码语言:txt复制 i ;
代码语言:txt复制 //方法1,直接实例化
代码语言:txt复制 user = new User();
代码语言:txt复制 //方法2,每次都通过反射获取class,然后实例化
代码语言:txt复制 user = (User) Class.forName("com.example.studynote.reflection.User").newInstance();
代码语言:txt复制 //方法3,通过之前反射得到的class进行实例化
代码语言:txt复制 user = (User) clz.newInstance();
代码语言:txt复制 }
代码语言:txt复制 System.out.println("耗时:" (System.currentTimeMillis() - startTime));
打印结果:
代码语言:txt复制1、直接实例化
代码语言:txt复制耗时:15
代码语言:txt复制2、每次都通过反射获取class,然后实例化
代码语言:txt复制耗时:671
代码语言:txt复制3、通过之前反射得到的class进行实例化
代码语言:txt复制耗时:31
所以看出来,只要我们合理的运用这些反射方法,比如Class.forName,Constructor,Method,Field
等,尽量在循环外就缓存好实例,就能提高反射的效率,减少耗时。
- 2、setAccessible(true)
之前我们说过当遇到私有变量和方法的时候,会用到setAccessible(true)
方法关闭安全检查。这个安全检查其实也是耗时的。
所以我们在反射的过程中可以尽量调用setAccessible(true)
来关闭安全检查,无论是否是私有的,这样也能提高反射的效率。
- 3、ReflectASM
ReflectASM 是一个非常小的 Java 类库,通过代码生成来提供高性能的反射处理,自动为 get/set 字段提供访问类,访问类使用字节码操作而不是 Java 的反射技术,因此非常快。
ASM是一个通用的Java字节码操作和分析框架。 它可以用于修改现有类或直接以二进制形式动态生成类。
简单的说,这是一个类似反射,但是不同于反射的高性能库。
他的原理是通过ASM库
,生成了一个新的类,然后相当于直接调用新的类方法,从而完成反射的功能。
感兴趣的可以去看看源码,实现原理比较简单——https://github.com/EsotericSoftware/reflectasm。
小总结:
经过上述三种方法,我想反射也不会那么可怕到大大影响性能的程度了,如果真的发现反射影响了性能以及实际使用的情况,也许可以研究下,是否是因为没用对反射和没有处理好反射相关的缓存呢?
反射原理
如果我们试着查看这些反射方法的源码,会发现最终都会走到native
方法中,比如
getDeclaredField
方法会走到
public native Field getDeclaredField(String name) throws NoSuchFieldException;
那么在底层,是怎么获取到类的相关信息的呢?
首先回顾下JVM加载Java文件
的过程:
编译阶段
,.java文件会被编译成.class文件,.class文件是一种二进制文件,内容是JVM能够识别的机器码。.class文件
里面依次存储着类文件的各种信息,比如:版本号、类的名字、字段的描述和描述符、方法名称和描述、是不是public、类索引、字段表集合,方法集合等等数据。- 然后,JVM中的类加载器会读取字节码文件,取出二进制数据,加载到内存中,并且解析
.class
文件的信息。 - 类加载器会获取类的二进制字节流,在内存中生成代表这个类的
java.lang.Class
对象。 - 最后会开始类的生命周期,比如
连接、初始化
等等。
而反射,就是去操作这个 java.lang.Class
对象,这个对象中有整个类的结构,包括属性方法等等。
总结来说就是,.class
是一种有顺序的结构文件,而Class对象
就是对这种文件的一种表示,所以我们能从Class对象
中获取关于类的所有信息,这就是反射的原理。