今天说Java模块内容:反射。html
正常状况下,咱们知晓咱们要操做的类和对象是什么,能够直接操做这些对象中的变量和方法,好比一个User类:java
User user=new User(); user.setName("Bob");
可是有的场景,咱们没法正常去操做:android
等等状况。git
因此咱们就须要一种机制能让咱们去操做任意的类和对象。github
这种机制,就是反射
。简单的说,反射就是:面试
对于任意一个类
,都可以知道这个类的全部属性和方法;
对于任意一个对象
,都可以调用它的任意方法和属性。segmentfault
先设定一个User类:api
package com.example.testapplication.reflection; public class User { private int age; public String name; public User() { System.out.println("调用了User()"); } private User(int age, String name) { this.name = name; this.age = age; System.out.println("调用了User(age,name)"+"__age:"+age+"__name:"+name); } public User(String name) { this.name = name; System.out.println("调用了User(name)"+"__name:"+name); } private String getName() { System.out.println("调用了getName()"); return this.name; } private String setName(String name) { this.name = name; System.out.println("调用了setName(name)__"+name); return this.name; } public int getAge() { System.out.println("调用了getAge()"); return this.age; } }
主要有三种方法获取Class对象
:缓存
//一、根据类路径获取类对象 try { Class clz = Class.forName("com.example.testapplication.reflection.User"); } catch (ClassNotFoundException e) { e.printStackTrace(); } //二、直接获取 Class clz = User.class; //三、对象的getclass方法 Class clz = new User().getClass();
一、获取类全部构造方法安全
Class clz = User.class; //获取全部构造函数(不包括私有构造方法) Constructor[] constructors1 = clz.getConstructors(); //获取全部构造函数(包括私有构造方法) Constructor[] constructors2 = clz.getDeclaredConstructors();
二、获取类的单个构造方法
try { //获取无参构造函数 Constructor constructor1 = clz.getConstructor(); //获取参数为String的构造函数 Constructor constructor2 =clz.getConstructor(String.class); //获取参数为int,String的构造函数 Class[] params = {int.class,String.class}; Constructor constructor3 =clz.getDeclaredConstructor(params); } catch (NoSuchMethodException e) { e.printStackTrace(); }
须要注意的是,User(int age, String name)
为私有构造方法,因此须要使用getDeclaredConstructor
获取。
一、调用Class对象的newInstance
方法
这个方法只能调用无参构造函数,也就是Class
对象的newInstance
方法不能传入参数。
Object user = clz.newInstance();
二、调用Constructor
对象的newInstance
方法
Class[] params = {int.class,String.class}; Constructor constructor3 =clz.getDeclaredConstructor(params); constructor3.setAccessible(true); constructor3.newInstance(22,"Bob");
这里要注意下,虽然getDeclaredConstructor
能获取私有构造方法,可是若是要调用这个私有方法,须要设置setAccessible(true)
方法,不然会报错:
can not access a member of class com.example.testapplication.reflection.User with modifiers "private"
Class clz = User.class; Field field1 = clz.getField("name"); Field field2 = clz.getDeclaredField("age");
一样的,getField
获取public
类变量,getDeclaredField
能够获取全部变量(包括私有变量属性)。
因此通常直接用getDeclaredField便可。
接上例,获取类的属性后,能够去修改类实例的对应属性,好比咱们有个user
的实例对象,咱们来修改它的name和age。
//修改name,name为public属性 Class clz = User.class; Field field1 = clz.getField("name"); field1.set(user,"xixi"); //修改age,age为private属性 Class clz = User.class; Field field2 = clz.getDeclaredField("age"); field2.setAccessible(true); field2.set(user,123);
//获取getName方法 Method method1 = clz.getDeclaredMethod("getName"); //获取setName方法,带参数 Method method2 = clz.getDeclaredMethod("setName", String.class); //获取getage方法 Method method3 = clz.getMethod("getAge");
method1.setAccessible(true); Object name = method1.invoke(user); method2.setAccessible(true); method2.invoke(user, "xixi"); Object age = method3.invoke(user);
虽然反射很好用,增长了程序的灵活性,可是也有他的缺点:
性能问题
。因为用到动态类型(运行时才检查类型),因此反射的效率比较低。可是对程序的影响比较小,除非对性能要求比较高。因此须要在二者之间平衡。不够安全
。因为能够执行一些私有的属性和方法,因此可能会带来安全问题。不易读写
。固然这一点也有解决方案,好比jOOR库,可是不适用于Android定义为final的字段。Hook 技术又叫作钩子函数,在系统没有调用该函数以前,钩子程序就先捕获该消息,钩子函数先获得控制权,这时钩子函数既能够加工处理(改变)该函数的执行行为,还能够强制结束消息的传递。
在插件化中,咱们须要找到能够hook的点,而后进行一些插件的工做,好比替换Activity,替换mH
等等。这其中就用到大量反射的知识,这里以替换mH为例:
// 获取到当前的ActivityThread对象 Class<?> activityThreadClass = Class.forName("android.app.ActivityThread"); Field currentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread"); currentActivityThreadField.setAccessible(true); Object currentActivityThread = currentActivityThreadField.get(null); //获取这个对象的mH Field mHField = activityThreadClass.getDeclaredField("mH"); mHField.setAccessible(true); Handler mH = (Handler) mHField.get(currentActivityThread); //替换mh为咱们本身的HandlerCallback Field mCallBackField = Handler.class.getDeclaredField("mCallback"); mCallBackField.setAccessible(true); mCallBackField.set(mH, new MyActivityThreadHandlerCallback(mH));
动态代理的特色是不须要提早建立代理对象,而是利用反射机制
在运行时建立代理类,从而动态实现代理功能。
public class InvocationTest implements InvocationHandler { // 代理对象(代理接口) private Object subject; public InvocationTest(Object subject) { this.subject = subject; } @Override public Object invoke(Object object, Method method, Object[] args) throws Throwable { //代理真实对象以前 Object obj = method.invoke(subject, args); //代理真实对象以后 return obj; } }
咱们能够发现不少库都会用到注解,而获取注解的过程也会有反射的过程,好比获取Activity
中全部变量的注解:
public void getAnnotation(Activity activity){ Class clazz = activity.getClass(); //得到activity中的全部变量 Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); //获取变量上加的注解 MyAnnotation test = field.getAnnotation(MyAnnotation.class); //... } }
这种经过反射处理注解的方式称做运行时注解,也就是程序运行状态的时候才会去处理注解。
可是上文说过了,反射会在必定程度上影响到程序的性能,因此还有一种处理注解的方式:编译时注解。
所用到的注解处理工具是APT
。
APT是一种注解处理器,能够在编译时进行扫描和处理注解,而后生成java代码文件,这种方法对比反射就能比较小的影响到程序的运行性能。
这里就不说APT的使用了,下次会专门有章节提到~
final咱们应该都知道,修饰变量的时候表明是一个常量,不可修改。那利用反射能不能达到修改的效果呢?
咱们先试着修改一个用final修饰的String
变量。
public class User { private final String name = "Bob"; private final Student student = new Student(); public String getName() { return name; } public Student getStudent() { return student; } } User user = new User(); Class clz = User.class; Field field1 = null; try{ field1=clz.getDeclaredField("name"); field1.setAccessible(true); field1.set(user,"xixi"); System.out.println(user.getName()); }catch(NoSuchFieldException e){ e.printStackTrace(); }catch(IllegalAccessException e){ e.printStackTrace(); }
打印出来的结果,仍是Bob
,也就是没有修改到。
咱们再修改下student
变量试试:
field1 = clz.getDeclaredField("student"); field1.setAccessible(true); field1.set(user, new Student()); 打印: 修改前com.example.studynote.reflection.Student@77459877 修改后com.example.studynote.reflection.Student@72ea2f77
能够看到,对于正常的对象变量即便被final
修饰也是能够经过反射进行修改的。
这是为何呢?为何String
不能被修改,而普通的对象变量能够被修改呢?
先说结论,其实String
值也被修改了,只是咱们没法经过这个对象获取到修改后的值。
这就涉及到JVM的内联优化
了:
内联函数,编译器将指定的函数体插入并取代每一处调用该函数的地方(上下文),从而节省了每次调用函数带来的额外时间开支。
简单的说,就是JVM在处理代码的时候会帮咱们优化代码逻辑,好比上述的final变量
,已知final
修饰后不会被修改,因此获取这个变量的时候就直接帮你在编译阶段就给赋值了。
因此上述的getName
方法通过JVM编译内联优化后会变成:
public String getName() { return "Bob"; }
因此不管怎么修改,都获取不到修改后的值。
有的朋友可能提出直接获取name呢?好比这样:
//修改成public public final String name = "Bob"; //反射修改后,打印user.name field1=clz.getDeclaredField("name"); field1.setAccessible(true); field1.set(user,"xixi"); System.out.println(user.name);
很差意思,仍是打印出来Bob。这是由于System.out.println(user.name)
这一句在通过编译后,会被写成:
System.out.println(user.name) //通过内联优化 System.out.println("Bob")
因此:
反射是能够修改final变量的,可是若是是基本数据类型或者String类型的时候,没法经过对象获取修改后的值,由于JVM对其进行了内联优化。
那有没有办法获取修改后的值呢?
有,能够经过反射中的Field.get(Object obj)
方法获取:
//获取field对应的变量在user对象中的值 System.out.println("修改后"+field.get(user));
说完了final,再说说static
,怎么修改static修饰的变量呢?
咱们知道,静态变量是在类的实例化以前就进行了初始化(类的初始化阶段)
,因此静态变量是跟着类自己走的,跟具体的对象无关,因此咱们获取变量就不须要传入对象,直接传入null便可:
public class User { public static String name; } field2 = clz.getDeclaredField("name"); field2.setAccessible(true); //获取静态变量 Object getname=field2.get(null); System.out.println("修改前"+getname); //修改静态变量 field2.set(null, "xixi"); System.out.println("修改后"+User.name);
如上述代码:
Field.get(null)
能够获取静态变量。Field.set(null,object)
能够修改静态变量。利用缓存,其实我不说你们也都知道,在平时项目中用到屡次的对象也会进行缓存,谁也不会屡次去建立。
可是,这一点在反射中尤其重要,好比Class.forName
方法,咱们作个测试:
long startTime = System.currentTimeMillis(); Class clz = Class.forName("com.example.studynote.reflection.User"); User user; int i = 0; while (i < 1000000) { i++; //方法1,直接实例化 user = new User(); //方法2,每次都经过反射获取class,而后实例化 user = (User) Class.forName("com.example.studynote.reflection.User").newInstance(); //方法3,经过以前反射获得的class进行实例化 user = (User) clz.newInstance(); } System.out.println("耗时:" + (System.currentTimeMillis() - startTime));
打印结果:
一、直接实例化 耗时:15 二、每次都经过反射获取class,而后实例化 耗时:671 三、经过以前反射获得的class进行实例化 耗时:31
因此看出来,只要咱们合理的运用这些反射方法,好比Class.forName,Constructor,Method,Field
等,尽可能在循环外就缓存好实例,就能提升反射的效率,减小耗时。
以前咱们说过当遇到私有变量和方法的时候,会用到setAccessible(true)
方法关闭安全检查。这个安全检查其实也是耗时的。
因此咱们在反射的过程当中能够尽可能调用setAccessible(true)
来关闭安全检查,不管是不是私有的,这样也能提升反射的效率。
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、类索引、字段表集合,方法集合等等数据。.class
文件的信息。 java.lang.Class
对象。链接、初始化
等等。而反射,就是去操做这个 java.lang.Class
对象,这个对象中有整个类的结构,包括属性方法等等。
总结来讲就是,.class
是一种有顺序的结构文件,而Class对象
就是对这种文件的一种表示,因此咱们能从Class对象
中获取关于类的全部信息,这就是反射的原理。
Android体系架构连接 (连载体系化文章、脑图、面试专题):https://github.com/JiMuzz/Android-Architecture
欢迎关注和Star~❤️
https://juejin.cn/post/6844903905483030536
https://www.zhihu.com/question/46883050
https://juejin.cn/post/6917984253360177159
https://blog.csdn.net/PiaoMiaoXiaodao/article/details/79871313
http://www.javashuo.com/article/p-rxkeejfb-eg.html
https://www.jianshu.com/p/3382cc765b39
http://www.javashuo.com/article/p-mutzfuyk-kr.html
有一块儿学习的小伙伴能够关注下❤️ 个人公众号——码上积木,天天剖析一个知识点,咱们一块儿积累知识,造成完总体系架构。公众号回复111可得到《面试题思考与解答》以往期刊。