反射技巧讓你的效能提升N倍

語言: CN / TW / HK

Hi 大家好,我是 DHL。公眾號:ByteCode ,專注分享有趣硬核原創內容,Kotlin、Jetpack、效能優化、系統原始碼、演算法及資料結構、動畫、大廠面經

在之前的文章和影片中我們拆分了不同的場景對比反射的效能。

在之前的文章中提到了一個提升效能非常重要的點,將 Accessible 設定 true 反射速度會進一步提升,如果單看一個程式,可能這點效能微不足道,但是如果放在一個大的複雜的工程下面,執行在大量的低端機下,一行程式碼提升的效能,可能比你寫 100 行程式碼提升的效能更加顯著。

而今天這篇文章從原始碼的角度分析一下 isAccessible() 方法的作用,為什麼將 Accessible 設定為 true 可以提升效能,在開始分析之前,我們先寫一段程式碼。

  • 宣告一個普通類,裡面有個 public 方法 getName()private 方法 getAddress()

``` class Person { public fun getName(): String { return "I am DHL" }

private fun getAddress(): String {
    return "BJ"
}

} ```

  • 通過反射獲取 getName()getAddress() 方法,花 3 秒鐘思考一下,下面的程式碼輸出的結果

``` // public 方法 val method1 = Person::class.declaredFunctions.find { it.name == "getName" } println("access = ${method1?.isAccessible}")

// private 方法 val method2 = Person::class.declaredFunctions.find { it.name == "getAddress" } println("access = ${method2?.isAccessible}") ```

無論是呼叫 public getName() 方法還是呼叫 private getAddress() 方法,最後輸出的結果都為 false,通過這個例子也間接說明了 isAccessible() 方法並不是用來表示訪問許可權的。

當我們通過反射呼叫 private 方法時,都需要執行 setAccessible() 方法設定為 true, 否者會丟擲下面的異常。

java.lang.IllegalAccessException: can not access a member of class com.hi.dhl.demo.reflect.Person

如果通過反射呼叫 public 方法,不設定 Accessibletrue,也可以正常呼叫,所以有很多小夥伴認為 isAccessible() 方法用來表示訪問許可權,其實這種理解是錯誤的。

我們一起來看一下原始碼是如何解釋的,方法 isAccessible() 位於 AccessibleObject 類中。

``` public class AccessibleObject implements AnnotatedElement { ...... // NOTE: for security purposes, this field must not be visible boolean override;

public boolean isAccessible() {
    return override;
}

public void setAccessible(boolean flag) throws SecurityException {
   ......
}
......

} ```

AccessibleObjectFieldMethodConstructor 的父類,呼叫 isAccessible() 返回 override 的值,而欄位 override 主要判斷是否要進行安全檢查。

欄位 overrideAccessibleObject 子類當中使用,所以我們一起來看一下它的子類 Method

public Object invoke(Object obj, Object... args){ // 是否要進行安全檢查 if (!override) { // 進行快速驗證是否是 Public 方法 if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { // 返回呼叫這個方法的 Class Class<?> caller = Reflection.getCallerClass(); // 做許可權訪問的校驗,快取呼叫這個方法的 Class,避免下次在做檢查 checkAccess(caller, clazz, obj, modifiers); } } ...... return ma.invoke(obj, args); }

欄位 override 提供給子類去重寫,它的值決定了是否要進行安全檢查,如果要進行安全檢查,則會執行 quickCheckMemberAccess() 快速驗證是否是 Public 方法,避免呼叫 getCallerClass()

  • 如果是 Public 方法,避免做安全檢查,所以我們在程式碼中不呼叫 setAccessible(true) 方法,也不會丟擲異常
  • 如果不是 Public 方法則會呼叫 getCallerClass() 獲取呼叫這個方法的 Class,執行 checkAccess() 方法進行安全檢查。

``` // it is necessary to perform somewhat expensive security checks. // A more complicated security check cache is needed for Method and Field // The cache can be either null (empty cache) volatile Object securityCheckCache; // 快取呼叫這個方法的 Class

void checkAccess(Class<?> caller, Class<?> clazz, Object obj, int modifiers){ ...... Object cache = securityCheckCache; // read volatile

if(cache == 呼叫這個方法的 Class){
    return;     // ACCESS IS OK
}

slowCheckMemberAccess(caller, clazz, obj, modifiers, targetClass);
......

}

void slowCheckMemberAccess(Class<?> caller, Class<?> clazz, Object obj, int modifiers,Class<?> targetClass){ Reflection.ensureMemberAccess(caller, clazz, obj, modifiers); Object cache = 呼叫這個方法的 Class securityCheckCache = cache; // 快取呼叫這個方法的 Class } ```

原始碼中註釋也說明了,如果要進行安全檢查那麼它的代價是非常昂貴的,所以用變數 securityCheckCache 快取呼叫這個方法的 Class。如果下次使用相同的 Class,就不需要在做安全檢查,但是這個快取有個缺陷,如果換一個呼叫這個方法的 Class,需要再次做安全檢查,並且會覆蓋之前的快取結果。

如果要在執行時修改屬性或者呼叫某個方法時,都要進行安全檢查,而安全檢查是非常消耗資源的,所以 JDK 提供了一個 setAccessible() 方法,可以繞過安全檢查,讓開發者自己來決定是否要避開安全檢查。

因為反射本身是非常慢的,如果能夠避免安全檢查,可以進一步提升效能,在之前的文章 揭祕反射真的很耗時嗎,射 10 萬次耗時多久,針對不同場景,分別測試了反射前後以及關閉安全檢查的耗時。

| | 正常呼叫 | 反射 | 反射優化後 | 反射優化後關掉安全檢查 | | --- | --- | --- | --- | --- | | 建立物件 | 0.578 ms/op| 4.710 ms/op | 1.018 ms/op | 0.943 ms/op | | 方法呼叫 | 0.422 ms/op | 10.533 ms/op | 0.844 ms/op | 0.687 ms/op | | 屬性呼叫 | 0.241 ms/op | 12.432 ms/op | 1.362 ms/op | 1.202 ms/op | | 伴生物件 | 0.470 ms/op | 5.661 ms/op | 0.840 ms/op | 0.702 ms/op |

從測試結果可以看出來,執行 setAccessible() 方法,設定為 true 關掉安全檢查之後,反射速度得到了進一步的提升,更接近於正常呼叫。

我正在參與掘金技術社群創作者簽約計劃招募活動,點選連結報名投稿


全文到這裡就結束了,感謝你的閱讀,堅持原創不易,歡迎在看、點贊、分享給身邊的小夥伴,我會持續分享原創乾貨!!!

真誠推薦你關注我,公眾號:ByteCode ,持續分享硬核原創內容,Kotlin、Jetpack、效能優化、系統原始碼、演算法及資料結構、動畫、大廠面經。



近期必讀熱門文章

最後推薦長期更新和維護的專案

  • 個人部落格,將所有文章進行分類,歡迎前去檢視 https://hi-dhl.com

  • KtKit 小巧而實用,用 Kotlin 語言編寫的工具庫,歡迎前去檢視 KtKit

  • 計劃建立一個最全、最新的 AndroidX Jetpack 相關元件的實戰專案以及相關元件原理分析文章,正在逐漸增加 Jetpack 新成員,倉庫持續更新,歡迎前去檢視 AndroidX-Jetpack-Practice

  • LeetCode / 劍指 offer / 國內外大廠面試題 / 多執行緒題解,語言 Java 和 kotlin,包含多種解法、解題思路、時間複雜度、空間複雜度分析