作為寫了幾年Java代碼的小菜雞,反射這個知識點一直只停留在知道、了解的水平,最近在看Android插件化方面的知識,有點小吃力,反思之后-v-,最終想明白了,是無知限制了我的想象力,痛定思痛,于是有了這篇Java反射的總結(jié)--送給自己。
反射的定義
Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions. The API accommodates applications that need access to either the public members of a target object (based on its runtime class) or the members declared by a given class. It also allows programs to suppress default reflective access control.
蹩腳大意翻譯:反射能夠讓Java代碼獲取一個已經(jīng)加載的類的字段,方法,構(gòu)造器等信息,并能夠訪問它們不受訪問權(quán)限的控制(private也能訪問)
上面反射是Oracle官方文檔的定義,反射能夠突破訪問權(quán)限控制,這還是很優(yōu)秀的,但是,問題來了,為什么需要反射或者說什么情況下需要用反射?
為什么需要反射
我們平時在IDE中寫的Java代碼,工程中新建的類,只要包含該類的包,就能訪問該類的公共方法,然后類的稀有方法被封裝隱藏起來,這一切都看起來很合理啊,那為什么還需要反射呢?其實一切都為了動態(tài)性,譬如以下幾種情況
- 一個類是從云端下載的,或者其他文件路徑加載進(jìn)來的,那么怎么使用該類呢,我們需要反射
- 框架為了封裝性,肯定會盡量暴露最少的信息給外部類使用,站在技術(shù)的角度沒毛病,可是需求總是善變的(還記得根據(jù)手機殼顏色設(shè)置app主題?)所以有時候我需要更大的權(quán)限,我們需要反射
- Java的動態(tài)代理,代理類是動態(tài)生成的,我們需要反射
當(dāng)然,反射還有很多其他應(yīng)用,它們的根本目的就是為了語言的動態(tài)性。
初識反射
我們都知道Java世界中一切皆對象,要學(xué)習(xí)使用反射,首先我們要理解Class對象,以最常見的HotSpot虛擬機為例,被加載的類都會被保存到方法區(qū),比如基礎(chǔ)類型 int.class , double.class 等,還有 容器庫 ArrayList.class, HashMap.class 等,所有的類的元數(shù)據(jù)都在方法區(qū),看一個例子
ArrayList arrayList = new ArrayList();
就一行代碼,新建了一個ArrayList對象, 那么arrayList指針會被push進(jìn)當(dāng)前調(diào)用的方法棧中,我們可以想的更進(jìn)一步,Java虛擬機是如何知道arrayList的真實類型的?要解釋這個,我還要了解Java對象頭的一些知識
Java對象頭
還是以HotSpot為例,對象在內(nèi)存中的數(shù)據(jù)分三部分:
- 對象頭 (我們關(guān)注的重點)
- 實例數(shù)據(jù) (很好理解,就是類對象成員變量等數(shù)據(jù))
- 字節(jié)對齊填充 (虛擬機規(guī)范要求對象起始地址必須是8的倍數(shù))
這個對象頭中包含的數(shù)據(jù)很多,包括哈希嗎,GC分代年齡,鎖狀態(tài)標(biāo)志,線程持有的鎖等,這些這里不展開說了,大家想了解可以自行查閱相關(guān)資料,除了這些還有一個很重要的指針-類型指針(指向類元數(shù)據(jù)的指針),剛好解釋了我上面的疑問,虛擬機通過這個指針來獲取該對象的真實類型。
反射的常規(guī)操作
要使用反射我們就要獲得Class對象,獲取方法有以下三種
- ArrayList.class
- new ArrayList().getClass()
- Class.forName("java.util.ArrayList")
這三種方法都可以獲取到對象對應(yīng)的Class對象,具體使用哪個要看具體情況,比如你只有該類的類名,那么可以選擇第三種,再比如你已經(jīng)有一個該類的類對象,那么可以選擇第二種方法。這里要注意一點,第一種方法只會觸發(fā)類的加載不會觸發(fā)類的初始化,第二,三種方法會同時觸發(fā)類加載和初始化(如果需要的話)。 關(guān)于這個 我們寫一個最簡單的demo測試一下
public class TestClassLoader {
static {
L.d("TestClassLoader class init!");
}
}
...
public static void main(String[] args) {
Class<?> clazz = TestClassLoader.class;
L.d("after TestClassLoader.class");
try {
clazz = Class.forName("com.aliouswang.practice.olympic.bean.TestClassLoader");
L.d("after Class.forName");
TestClassLoader testClassLoader = (TestClassLoader) clazz.newInstance();
L.d("after new instance");
} catch (Exception e) {
e.printStackTrace();
}
}
//控制臺打印的結(jié)果
after TestClassLoader.class
TestClassLoader class init! static value is 100
after Class.forName
after new instance
可以看到TestClassLoader 類的靜態(tài)代碼塊在Class.forName()方法調(diào)用之前被初始化,而調(diào)用TestClassLoader.class時并沒有觸發(fā)初始化,這個簡單的例子也就驗證了我們上面的結(jié)論。
拿到Class對象之后,我們可以操作以下幾類對象 類實現(xiàn)的接口、類的內(nèi)部類、類的構(gòu)造器、類的成員變量、類的方法,Class都提供了對應(yīng)的方法來獲取它們。
- getInterfaces() -- 返回當(dāng)前類實現(xiàn)的所有接口(不包括從父類繼承來的)
- getClasses() -- 返回當(dāng)前類和從父類繼承來的public內(nèi)部類
- getDeclaredClasses() -- 返回當(dāng)前類的所有內(nèi)部類(包括private類型,但是不包括從父類繼承來的)
- getConstructors() -- 返回當(dāng)前類所有的public構(gòu)造器
- getDeclaredConstructors() -- 返回當(dāng)前類所有的構(gòu)造器(包括private類型)
- getConstructor(Class<?>... parameterTypes) -- 根據(jù)參數(shù),返回最匹配的構(gòu)造器對象
- getMethods() -- 返回當(dāng)前類和從父類繼承來的所有public方法
- getDeclaredMethods() -- 返回當(dāng)前類所有的Method方法(包括private類型)
- getDeclaredMethod(String name, Class<?>... parameterTypes) -- 根據(jù)參數(shù),返回最匹配的方法
- getFields() -- 返回當(dāng)前類和從父類繼承來的public字段
- getDeclaredFields() -- 返回當(dāng)前類定義的所有字段(包括private)
- getDeclaredField(String name) --返回當(dāng)前類定義的字段通過參數(shù)
上面這些方法不需要死記硬背,需要用到的時候,查看一下文檔或者方法說明即可,下面舉2個栗子來加深理解。
第一個栗子
需求:現(xiàn)在有個Apple類,它繼承于Fruit類,F(xiàn)ruit有一個私有方法seal 參數(shù)是一個float類型, 現(xiàn)在要求我們通過反射來調(diào)用該私有方法
class Apple extends Fruit{}
class Furit {
public Fruit(int price) {this.price = price;}
private void seal(int price) {
L.d("This fruit is sealed by pruce : ¥" + price);
}
}
private void invokeFruitSeal() {
try {
//獲取Apple類
Class clazz = Class.forName("com.aliouswang.practice.olympic.bean.Apple");
//獲取Apple的直接父類Fruit
Class fruitClazz = clazz.getSuperclass();
//獲取父類的私有構(gòu)造器
Constructor<?> constructor = fruitClazz.getDeclaredConstructor(int.class);
//設(shè)置私有可訪問
constructor.setAccessible(true);
//通過私有構(gòu)造器新建Fruit對象
Fruit fruit = (Fruit) constructor.newInstance(0);
//獲取fruit的私有方法
Method sealMethod = fruitClazz.getDeclaredMethod("seal", float.class);
sealMethod.setAccessible(true);
//調(diào)用方法
sealMethod.invoke(fruit, 998.8f);
} catch (Exception e) {
e.printStackTrace();
}
}
// 查看控制臺打印的結(jié)果,調(diào)用成功
This fruit is sealed by pruce : ¥998.8
再舉一個栗子
需求:這次我們在Fruit類中新建一個Size類,然后新增一個final的Size常量,我們需要利用反射對其進(jìn)行修改(正常final常量初始化之后是不能夠被修改的,但是利用反射能夠做到-。-)
class Fruit {
private final Size size = new Size(50);
@Override
public String toString() {
return "fruit size is : " + size;
}
}
private void modifyFruitSize() {
try {
//獲取Apple類
Class clazz = Class.forName("com.aliouswang.practice.olympic.bean.Apple");
//獲取Apple的直接父類Fruit
Class fruitClazz = clazz.getSuperclass();
//獲取父類的私有構(gòu)造器
Constructor<?> constructor = fruitClazz.getDeclaredConstructor(int.class);
//設(shè)置私有可訪問
constructor.setAccessible(true);
//通過私有構(gòu)造器新建Fruit對象
Fruit fruit = (Fruit) constructor.newInstance(0);
//修改之前打印fruit size
L.d("before modify fruit size" + fruit);
//獲取size字段
Field sizeField = fruitClazz.getDeclaredField("size");
//設(shè)置私有可訪問
sizeField.setAccessible(true);
//我們利用反射將size改為非final類型
Field modifierField = sizeField.getClass().getDeclaredField("modifiers");
modifierField.setAccessible(true);
//修改類型
int modifiers = sizeField.getModifiers() & ~Modifier.FINAL;
modifierField.set(sizeField, modifiers);
Size size = new Size(998);
//設(shè)置新的size
sizeField.set(fruit, size);
L.d("after modify fruit size" + fruit);
} catch (Exception e) {
e.printStackTrace();
}
}
// 查看控制臺打印的結(jié)果,調(diào)用成功
before modify fruit sizefruit size is : radius is 50
after modify fruit sizefruit size is : radius is 998
上面我們通過2個小栗子,熟悉了Java反射的基本操作,除了上面第二個栗子中修改size的final屬性稍微難理解一點,還是比較簡單的,不過凡事都有一個熟悉的過程,掌握了這些基礎(chǔ)的用法,遇到新的需求,能不能使用發(fā)射去完成,我們心中就有數(shù)了。
如果你覺得這2個小栗子還不過癮的話,我再看來一個更實際的案例Android Activity 啟動 Hook。
Android Activity 啟動 Hook
當(dāng)然關(guān)于Android Activity 啟動 Hook的技術(shù)網(wǎng)上很多,也有很多很優(yōu)秀的開源項目,我在這里談這個有點 關(guān)公面前耍大刀的感覺了,但是我在這里談這個只是為了說明 利用反射 我們可以做很多事情,
所以我就找了這么一個切入點來展示反射能做什么事情,權(quán)當(dāng)拋磚引玉。
我們的需求是,在啟動activity時,打印一個Log日志,廢話不多說,我們開始。
我們這里只Hook Activity.startActivity 這種方式的啟動, 看下android framework的相關(guān)源碼
public void startActivity(Intent intent) {
//常用的activity.startActivity方法
this.startActivity(intent, null);
}
//...
@Override
public void startActivity(Intent intent, @Nullable Bundle options) {
if (options != null) {
startActivityForResult(intent, -1, options);
} else {
// Note we want to go through this call for compatibility with
// applications that may have overridden the method.
startActivityForResult(intent, -1);
}
}
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
@Nullable Bundle options) {
if (mParent == null) {
options = transferSpringboardActivityOptions(options);
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
//...
}
//...
}
private Instrumentation mInstrumentation;
我們看到activity.startActivity方法,最終去執(zhí)行啟動操作會用到mInstrumentation這個私有成員變量,所以自然想到它是一個很好的Hook點,分下面三步來走
- 第一步,先獲取到該Activity的mInstrumentation
- 第二步,新建一個新的Instrumentation類,重寫execStartActivity方法,在執(zhí)行父類的方法之前加入我們需要的Log日志
- 第三步,將我們新建的新的Instrumentation對象,設(shè)置給activity
第一步
public static void hook(Activity activity) {
try {
Field instrumentationField = Activity.class.getDeclaredField("mInstrumentation");
instrumentationField.setAccessible(true);
//...
}
}
第二步
public class HookInstrumention extends Instrumentation{
private Instrumentation mTarget;
public HookInstrumention(Instrumentation target) {
this.mTarget = target;
}
public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
L.d("before start activity!");
Class superClass = Instrumentation.class;
try {
Method method = superClass.getDeclaredMethod("execStartActivity",
Context.class, IBinder.class, IBinder.class, Activity.class,
Intent.class, int.class, Bundle.class);
method.setAccessible(true);
return (ActivityResult) method.invoke(this.mTarget, who, contextThread, token, target, intent, requestCode, options);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
}
第三步
public static void hook(Activity activity) {
try {
Field instrumentationField = Activity.class.getDeclaredField("mInstrumentation");
instrumentationField.setAccessible(true);
Instrumentation instrumentation = (Instrumentation) instrumentationField.get(activity);
HookInstrumention hookInstrumention = new HookInstrumention(instrumentation);
instrumentationField.set(activity, hookInstrumention);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
//測試一下:
HookUtil.hook(this);
startActivity(new Intent(MainActivity.this, SecondActivity.class));
//查看log
08-27 14:43:33.391 10298-10298/com.aliouswang.practice.olympic E/hook: before start activity!
08-27 14:43:33.392 10298-10298/com.aliouswang.practice.olympic I/Timeline: Timeline: Activity_launch_request time:427958941 intent:Intent { cmp=com.aliouswang.practice.olympic/.SecondActivity }
可以看到我們在Activity 啟動之前,成功的打印了一條日志??!
反射的缺點
凡事有得必有失,反射也有它的缺點,反射的缺點主要有2點。
- 我們通過反射獲得了靈活性,同時也要付出代價,我們會失去編譯器優(yōu)化我們代碼的機會,這樣我們的代碼執(zhí)行效率會低一些,但是隨著JDK版本的不斷升級,性能差距在不斷的縮小。
- 反射打破了我們代碼的封裝性,增加了維護成本。
為了能比較直觀的說明性能下降的問題,我決定做一個 小小的實驗,來直觀的對比一下,反射代碼與原生代碼的執(zhí)行效率對比.
public class RunSpeedTest {
public static void main(String[] args) {
int total = 1000000;
testNativeCode(total);
testReflectCode(total);
}
private static void testNativeCode(int total) {
long startTime = System.currentTimeMillis();
for (int i = 0; i < total; i++) {
String orinal = i + "";
String str = new String(orinal);
str.toUpperCase();
}
L.d("native code use time : " + (System.currentTimeMillis() - startTime) + "ms");
}
private static void testReflectCode(int total) {
long startTime = System.currentTimeMillis();
for (int i = 0; i < total; i++) {
String orinal = i + "";
Class<?> strClazz = String.class;
String str = null;
try {
Constructor<?> constructor = strClazz.getDeclaredConstructor(String.class);
constructor.setAccessible(true);
str = (String) constructor.newInstance(orinal);
Method method = strClazz.getMethod("toUpperCase");
method.setAccessible(true);
method.invoke(str);
} catch (Exception e) {
e.printStackTrace();
}
}
L.d("reflect code use time : " + (System.currentTimeMillis() - startTime) + "ms");
}
}
我的電腦配置是macOS 14,CPU i7,內(nèi)存16g, 在new 1,000,000 個String對象并調(diào)用其toUpperCase方法的情況下,最終測試的結(jié)果如下
native code use time : 122ms
reflect code use time : 795ms
可以看到使用反射的代碼比正常編寫的代碼慢了6倍,雖然差距還是有的,但是這相差的600多ms的時間差平攤到每一次的循環(huán)中,那這點差距就顯得微不足道了,
所以對性能不是特別敏感的場景,使用反射性能是OK的,當(dāng)然編程不是為了炫技,我們需要擇優(yōu)選擇最好的實現(xiàn)方式, 如非必要還是少用反射。
以上??!各位看官,新人寫作不易,如果我的總結(jié)對你有一點點幫助,點個贊唄,小弟在此先行謝過啦?。?!-。-
我的博客即將搬運同步至騰訊云+社區(qū),邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=18g7tqdy5klup