前言
今天說Java模組內容:反射。
反射介紹
正常情況下,我們知曉我們要操作的類和物件是什麼,可以直接操作這些物件中的變數和方法,比如一個User類:
User user=new User();
user.setName("Bob");
但是有的場景,我們無法正常去操作:
- 只知道類路徑,無法直接例項化的物件。
- 無法直接操作某個物件的變數和方法,比如私有方法,私有變數。
- 需要hook系統邏輯,比如修改某個例項的引數。
等等情況。
所以我們就需要一種機制能讓我們去操作任意的類和物件。
這種機制,就是反射
。簡單的說,反射就是:
對於任意一個類
,都能夠知道這個類的所有屬性和方法;
對於任意一個物件
,都能夠呼叫它的任意方法和屬性。
常用API舉例
先設定一個User類:
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物件
主要有三種方法獲取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 = new User().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 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的欄位。
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);
//獲取這個物件的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我們應該都知道,修飾變數的時候代表是一個常量,不可修改。那利用反射能不能達到修改的效果呢?
我們先試著修改一個用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));
反射獲取static靜態變數
說完了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)
可以修改靜態變數。
怎麼提升反射效率
- 1、快取重複用到的物件
利用快取,其實我不說大家也都知道,在平時專案中用到多次的物件也會進行快取,誰也不會多次去建立。
但是,這一點在反射中尤為重要,比如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));
列印結果:
1、直接例項化
耗時:15
2、每次都通過反射獲取class,然後例項化
耗時:671
3、通過之前反射得到的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
方法會走到
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物件
中獲取關於類的所有資訊,這就是反射的原理。
歡迎關注
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
https://www.cnblogs.com/coding-night/p/10772631.html
https://www.jianshu.com/p/3382cc765b39
https://segmentfault.com/a/1190000015860183
拜拜
有一起學習的小夥伴可以關注下❤️ 我的公眾號——碼上積木,每天剖析一個知識點,我們一起積累知識,形成完整體系架構。公眾號回覆111可獲得《面試題思考與解答》以往期刊。