今天說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可得到《面試題思考與解答》以往期刊。