Java反射全解析(使用、原理、问题、在Android中的应用)

前言

今天说Java模块内容:反射。

反射介绍

正常情况下,我们知晓我们要操作的类和对象是什么,可以直接操作这些对象中的变量和方法,比如一个User类:

User user=newUser();user.setName("Bob");

但是有的场景,我们无法正常去操作:

只知道类路径,无法直接实例化的对象。

无法直接操作某个对象的变量和方法,比如私有方法,私有变量。

需要hook系统逻辑,比如修改某个实例的参数。

等等情况。

所以我们就需要一种机制能让我们去操作任意的类和对象。

这种机制,就是反射。简单的说,反射就是:

对于任意一个类,都能够知道这个类的所有属性和方法;

对于任意一个对象,都能够调用它的任意方法和属性。

常用API举例

先设定一个User类:

packagecom.example.testapplication.reflection;publicclassUser{privateintage;publicString name;publicUser(){        System.out.println("调用了User()");    }privateUser(intage, String name){this.name = name;this.age = age;        System.out.println("调用了User(age,name)"+"__age:"+age+"__name:"+name);    }publicUser(String name){this.name = name;        System.out.println("调用了User(name)"+"__name:"+name);    }privateStringgetName(){        System.out.println("调用了getName()");returnthis.name;    }privateStringsetName(String name){this.name = name;        System.out.println("调用了setName(name)__"+name);returnthis.name;    }publicintgetAge(){        System.out.println("调用了getAge()");returnthis.age;    }    }

获取Class对象

主要有三种方法获取Class对象:

根据类路径获取类对象

直接获取

实例对象的getclass方法

//1、根据类路径获取类对象try{    Class clz = Class.forName("com.example.testapplication.reflection.User");}catch(ClassNotFoundException e) {    e.printStackTrace();}//2、直接获取Class clz = User.class;//3、对象的getclass方法Class clz =newUser().getClass();

获取类的构造方法

1、获取类所有构造方法

Class clz = User.class;//获取所有构造函数(不包括私有构造方法)Constructor[] constructors1 = clz.getConstructors();//获取所有构造函数(包括私有构造方法)Constructor[] constructors2 = clz.getDeclaredConstructors();

2、获取类的单个构造方法

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获取。

调用类的构造方法生成实例对象

1、调用Class对象的newInstance方法

这个方法只能调用无参构造函数,也就是Class对象的newInstance方法不能传入参数。

Object user = clz.newInstance();

2、调用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 ofclasscom.example.testapplication.reflection.Userwithmodifiers"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的字段。

Android中的应用

插件化(Hook)

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);//获取这个对象的mHField mHField = activityThreadClass.getDeclaredField("mH");mHField.setAccessible(true);Handler mH = (Handler) mHField.get(currentActivityThread);//替换mh为我们自己的HandlerCallbackField mCallBackField = Handler.class.getDeclaredField("mCallback");mCallBackField.setAccessible(true);mCallBackField.set(mH,newMyActivityThreadHandlerCallback(mH));

动态代理

动态代理的特点是不需要提前创建代理对象,而是利用反射机制在运行时创建代理类,从而动态实现代理功能。

publicclassInvocationTestimplementsInvocationHandler{// 代理对象(代理接口)privateObject subject;publicInvocationTest(Object subject){this.subject = subject;    }@OverridepublicObjectinvoke(Object object, Method method, Object[] args)throwsThrowable{//代理真实对象之前Object obj = method.invoke(subject, args);//代理真实对象之后returnobj;    }}

三方库(注解)

我们可以发现很多库都会用到注解,而获取注解的过程也会有反射的过程,比如获取Activity中所有变量的注解:

publicvoidgetAnnotation(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我们应该都知道,修饰变量的时候代表是一个常量,不可修改。那利用反射能不能达到修改的效果呢?

我们先试着修改一个用final修饰的String变量。

publicclassUser{privatefinalString name ="Bob";privatefinalStudent student =newStudent();publicStringgetName(){returnname;    }publicStudentgetStudent(){returnstudent;    }}    User user =newUser();    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,newStudent());打印:修改前com.example.studynote.reflection.Student@77459877修改后com.example.studynote.reflection.Student@72ea2f77

可以看到,对于正常的对象变量即使被final修饰也是可以通过反射进行修改的。

这是为什么呢?为什么String不能被修改,而普通的对象变量可以被修改呢?

先说结论,其实String值也被修改了,只是我们无法通过这个对象获取到修改后的值。

这就涉及到JVM的内联优化了:

内联函数,编译器将指定的函数体插入并取代每一处调用该函数的地方(上下文),从而节省了每次调用函数带来的额外时间开支。

简单的说,就是JVM在处理代码的时候会帮我们优化代码逻辑,比如上述的final变量,已知final修饰后不会被修改,所以获取这个变量的时候就直接帮你在编译阶段就给赋值了。

所以上述的getName方法经过JVM编译内联优化后会变成:

publicStringgetName(){return"Bob";    }

所以无论怎么修改,都获取不到修改后的值。

有的朋友可能提出直接获取name呢?比如这样:

//修改为publicpublicfinalString name ="Bob";//反射修改后,打印user.namefield1=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));

反射获取static静态变量

说完了final,再说说static,怎么修改static修饰的变量呢?

我们知道,静态变量是在类的实例化之前就进行了初始化(类的初始化阶段),所以静态变量是跟着类本身走的,跟具体的对象无关,所以我们获取变量就不需要传入对象,直接传入null即可:

publicclassUser{publicstaticString 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) 可以修改静态变量。

怎么提升反射效率

1、缓存重复用到的对象

利用缓存,其实我不说大家也都知道,在平时项目中用到多次的对象也会进行缓存,谁也不会多次去创建。

但是,这一点在反射中尤为重要,比如Class.forName方法,我们做个测试:

longstartTime = System.currentTimeMillis();    Class clz = Class.forName("com.example.studynote.reflection.User");    User user;inti =0;while(i <1000000) {        i++;//方法1,直接实例化user =newUser();//方法2,每次都通过反射获取class,然后实例化user = (User) Class.forName("com.example.studynote.reflection.User").newInstance();//方法3,通过之前反射得到的class进行实例化user = (User) clz.newInstance();    }    System.out.println("耗时:"+ (System.currentTimeMillis() - startTime));

打印结果:

1、直接实例化耗时:152、每次都通过反射获取class,然后实例化耗时:6713、通过之前反射得到的class进行实例化耗时: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方法会走到

publicnativeFieldgetDeclaredField(String name)throwsNoSuchFieldException;

那么在底层,是怎么获取到类的相关信息的呢?

首先回顾下JVM加载Java文件的过程:

编译阶段,.java文件会被编译成.class文件,.class文件是一种二进制文件,内容是JVM能够识别的机器码。

.class文件里面依次存储着类文件的各种信息,比如:版本号、类的名字、字段的描述和描述符、方法名称和描述、是不是public、类索引、字段表集合,方法集合等等数据。

然后,JVM中的类加载器会读取字节码文件,取出二进制数据,加载到内存中,并且解析.class文件的信息。

类加载器会获取类的二进制字节流,在内存中生成代表这个类的java.lang.Class对象。

最后会开始类的生命周期,比如连接、初始化等等。

而反射,就是去操作这个 java.lang.Class对象,这个对象中有整个类的结构,包括属性方法等等。

总结来说就是,.class是一种有顺序的结构文件,而Class对象就是对这种文件的一种表示,所以我们能从Class对象中获取关于类的所有信息,这就是反射的原理。

文章来源:http://www.feixueteam.net/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容