揭秘反射真的很耗時嗎,射 10 萬次用時多久
全文分為
影片版
和
文字版
,
文字版:
側重於細節上的知識點更多、更加詳細
影片版:
透過動畫展示講解,更加的清楚、直觀
影片版本地址:https://b23。tv/Hprua24
無論是在面試過程中,還是看網路上各種技術文章,只要提到反射,不可避免都會提到一個問題,反射會影響效能嗎?影響有多大?如果在寫業務程式碼的時候,你用到了反射,都會被 review 人發出靈魂拷問,為什麼要用反射,有沒有其它的解決辦法。
而網上的答案都是千篇一律,比如反射慢、反射過程中頻繁的建立物件佔用更多記憶體、頻繁的觸發 GC 等等。那麼反射慢多少?反射會佔用多少記憶體?建立 1 個物件或者建立 10 萬個物件耗時多少?單次反射或者 10 萬次反射耗時多少?在我們的腦海中沒有一個直觀的概念,而今天這篇文章將會告訴你。
這篇文章,設計了幾個常用的場景,一起討論一下反射是否真的很耗時?最後會以圖表的形式展示。
測試工具及方案
在開始之前我們需要定義一個反射類
Person
。
class Person {
var age = 10
fun getName(): String {
return “I am DHL”
}
companion object {
fun getAddress(): String = “BJ”
}
}
針對上面的測試類,設計了以下幾個常用的場景,驗證反射前後的耗時。
建立物件
方法呼叫
屬性呼叫
伴生物件
測試工具及程式碼:
JMH (Java Microbenchmark Harness),這是 Oracle 開發的一個基準測試工具,他們比任何人都瞭解 JIT 以及 JVM 的最佳化對測試過程中的影響,所以使用這個工具可以儘可能的保證結果的可靠性。
基準測試是測試應用效能的一種方法,在特定條件下對某一物件的效能指標進行測試
本文的測試程式碼已經上傳到
github 倉庫 KtPractice
歡迎前往檢視。
github 倉庫 KtPractice: https://github。com/hi-dhl/KtPractice
為什麼使用 JMH
因為 JVM 會對程式碼做各種最佳化,如果只是在程式碼前後列印時間戳,這樣計算的結果是不置信的,因為忽略了 JVM 在執行過程中,對程式碼進行最佳化產生的影響。而 JMH 會盡可能的減少這些最佳化對最終結果的影響。
測試方案
在單程序、單執行緒中,針對以上四個場景,每個場景測試五輪,每輪迴圈 10 萬次,計算它們的平均值
在執行之前,需要對程式碼進行預熱,預熱不會作為最終結果,預熱的目的是為了構造一個相對穩定的環境,保證結果的可靠性。因為 JVM 會對執行頻繁的程式碼,嘗試編譯為機器碼,從而提高執行速度。而預熱不僅包含編譯為機器碼,還包含 JVM 各種最佳化演算法,儘量減少 JVM 的最佳化,構造一個相對穩定的環境,降低對結果造成的影響。
JMH 提供 Blackhole,透過 Blackhole 的 consume 來避免 JIT 帶來的最佳化
Kotlin 和 Java 的反射機制
本文測試程式碼全部使用 Kotlin,Koltin 是完美相容 Java 的,所以同樣也可以使用 Java 的反射機制,但是 Kotlin 自己也封裝了一套反射機制,並不是用來取代 Java 的,是 Java 的增強版,因為 Kotlin 有自己的語法特點比如
擴充套件方法
、
伴生物件
、
可空型別的檢查
等等,如果想使用 Kotlin 反射機制,需要引入以下庫。
implementation “org。jetbrains。kotlin:kotlin-reflect:$kotlin_version”
在開始分析,我們需要對比 Java 瞭解一下 Kotlin 反射基本語法。
kotlin 的
KClass
對應 Java 的
Class
,我們可以透過以下方式完成
KClass
和
Class
之間互相轉化
// 獲取 Class
Person()。javaClass
Person()::class。java
Person::class。java
Class。forName(“com。hi-dhl。demo。Person”)
// 獲取 KClass
Person()。javaClass。kotlin
Person::class
Class。forName(“com。hi-dhl。demo。Person”)。kotlin
kotlin 的
KProperty
對應 Java 的
Field
,Java 的
Field
有
getter/setter
方法,但是在 Kotlin 中沒有
Field
,分為了
KProperty
和
KMutableProperty
,當變數用
val
宣告的時候,即屬性為
KProperty
,如果變數用
var
宣告的時候,即屬性為
KMutableProperty
// Java 的獲取方式
Person()。javaClass。getDeclaredField(“age”)
// Koltin 的獲取方式
Person::class。declaredMemberProperties。find { it。name == “age” }
在 Kotlin 中
函式
、
屬性
以及
建構函式
的超型別都是
KCallable
,對應的子型別是
KFunction
(函式、構造方法等等) 和
KProperty / KMutableProperty
(屬性),而 Kotlin 中的
KCallable
對應 Java 的
AccessibleObject
, 其子型別分別是
Method
、
Field
、
Constructor
// Java
Person()。javaClass。getConstructor()。newInstance() // 構造方法
Person()。javaClass。getDeclaredMethod(“getName”) // 成員方法
// Kotlin
Person::class。primaryConstructor?。call() // 構造方法
Person::class。declaredFunctions。find { it。name == “getName” } // 成員方法
無論是使用 Java 還是 Kotlin 最終測試出來的結論都是一樣的,瞭解完基本反射語法之後,我們分別測試上述四種場景反射前後的耗時。
建立物件
正常建立物件
@Benchmark
fun createInstance(bh: Blackhole) {
for (index in 0 until 100_000) {
bh。consume(Person())
}
}
五輪測試平均耗時
0。578 ms/op
。需要重點注意,這裡使用了 JMH 提供
Blackhole
,透過
Blackhole
的
consume()
方法來避免 JIT 帶來的最佳化, 讓結果更加接近真實。
在物件建立過程中,會先檢查類是否已經載入,如果類已經載入了,會直接為物件分配空間,其中最耗時的階段其實是類的載入過程(載入->驗證->準備->解析->初始化)。
透過反射建立物件
@Benchmark
fun createReflectInstance(bh: Blackhole) {
for (index in 0 until 100_000) {
bh。consume(Person::class。primaryConstructor?。call())
}
}
五輪測試平均耗時
4。710 ms/op
,是正常建立物件的
9。4 倍
,這個結果是很驚人,如果將中間操作(獲取構造方法)從迴圈中提取出來,那麼結果會怎麼樣呢。
反射最佳化
@Benchmark
fun createReflectInstanceAccessibleTrue(bh: Blackhole) {
val constructor = Person::class。primaryConstructor
for (index in 0 until 100_000) {
bh。consume(constructor?。call())
}
}
正如你所見,我將中間操作(獲取構造方法)從迴圈中提取出來,五輪測試平均耗時
1。018 ms/op
,速度得到了很大的提升,相比反射最佳化前速度提升了
4。7
倍,但是如果我們在將安全檢查功能關掉呢。
constructor?。isAccessible = true
isAccessible
是用來判斷是否需要進行安全檢査,設定為
true
表示關掉安全檢查,將會減少安全檢査產生的耗時,五輪測試平均耗時
0。943 ms/op
,反射速度進一步提升了。
幾輪測試最後的結果如下圖示。
方法呼叫
正常呼叫
@Benchmark
fun callMethod(bh: Blackhole) {
val person = Person()
for (index in 0 until 100_000) {
bh。consume(person。getName())
}
}
五輪測試平均耗時
0。422 ms/op
。
反射呼叫
@Benchmark
fun callReflectMethod(bh: Blackhole) {
val person = Person()
for (index in 0 until 100_000) {
val method = Person::class。declaredFunctions。find { it。name == “getName” }
bh。consume(method?。call(person))
}
}
五輪測試平均耗時
10。533 ms/op
,是正常呼叫的
26 倍
。如果我們將中間操作(獲取
getName
程式碼)從迴圈中提取出來,結果會怎麼樣呢。
反射最佳化
@Benchmark
fun callReflectMethodAccessiblFalse(bh: Blackhole) {
val person = Person()
val method = Person::class。declaredFunctions。find { it。name == “getName” }
for (index in 0 until 100_000) {
bh。consume(method?。call(person))
}
}
將中間操作(獲取
getName
程式碼)從迴圈中提取出來了,五輪測試平均耗時
0。844 ms/op
,速度得到了很大的提升,相比反射最佳化前速度提升了
13
倍,如果在將安全檢查關掉呢。
method?。isAccessible = true
五輪測試平均耗時
0。687 ms/op
,反射速度進一步提升了。
幾輪測試最後的結果如下圖示。
屬性呼叫
正常呼叫
@Benchmark
fun callPropertie(bh: Blackhole) {
val person = Person()
for (index in 0 until 100_000) {
bh。consume(person。age)
}
}
五輪測試平均耗時
0。241 ms/op
。
反射呼叫
@Benchmark
fun callReflectPropertie(bh: Blackhole) {
val person = Person()
for (index in 0 until 100_000) {
val propertie = Person::class。declaredMemberProperties。find { it。name == “age” }
bh。consume(propertie?。call(person))
}
}
五輪測試平均耗時
12。432 ms/op
,是正常呼叫的
62 倍
,然後我們將中間操作(獲取屬性的程式碼)從迴圈中提出來。
反射最佳化
@Benchmark
fun callReflectPropertieAccessibleFalse(bh: Blackhole) {
val person = Person::class。createInstance()
val propertie = Person::class。declaredMemberProperties。find { it。name == “age” }
for (index in 0 until 100_000) {
bh。consume(propertie?。call(person))
}
}
將中間操作(獲取屬性的程式碼)從迴圈中提出來之後,五輪測試平均耗時
1。362 ms/op
,速度得到了很大的提升,相比反射最佳化前速度提升了
8
倍,我們在將安全檢查關掉,看一下結果。
propertie?。isAccessible = true
五輪測試平均耗時
1。202 ms/op
,反射速度進一步提升了。
幾輪測試最後的結果如下圖示。
伴生物件
正常呼叫
@Benchmark
fun callCompaion(bh: Blackhole) {
for (index in 0 until 100_000) {
bh。consume(Person。getAddress())
}
}
五輪測試平均耗時
0。470 ms/op
。
反射呼叫
@Benchmark
fun createReflectCompaion(bh: Blackhole) {
val classes = Person::class
val personInstance = classes。companionObjectInstance
val personObject = classes。companionObject
for (index in 0 until 100_000) {
val compaion = personObject?。declaredFunctions?。find { it。name == “getAddress” }
bh。consume(compaion?。call(personInstance))
}
}
五輪測試平均耗時
5。661 ms/op
,是正常呼叫的
11 倍
,然後我們在看一下將中間操作(獲取
getAddress
程式碼)從迴圈中提出來的結果。
反射最佳化
@Benchmark
fun callReflectCompaionAccessibleFalse(bh: Blackhole) {
val classes = Person::class
val personInstance = classes。companionObjectInstance
val personObject = classes。companionObject
val compaion = personObject?。declaredFunctions?。find { it。name == “getAddress” }
for (index in 0 until 100_000) {
bh。consume(compaion?。call(personInstance))
}
}
將中間操作(獲取
getAddress
程式碼)從迴圈中提出來,五輪測試平均耗時
0。840 ms/op
,速度得到了很大的提升,相比反射最佳化前速度提升了
7
倍,現在我們在將安全檢查關掉。
compaion?。isAccessible = true
五輪測試平均耗時
0。702 ms/op
,反射速度進一步提升了。
幾輪測試最後的結果如下圖所示。
總結
我們對比了四種常用的場景:
建立物件
、
方法呼叫
、
屬性呼叫
、
伴生物件
。分別測試了反射前後的耗時,最後彙總一下五輪 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
每個場景反射前後的耗時如下圖所示。
在我們的印象中,反射就是惡魔,影響會非常大,但是從上面的表格看來,反射確實會有一定的影響,但是如果我們合理使用反射,最佳化後的反射結果並沒有想象的那麼大,這裡有幾個建議。
在頻繁的使用反射的場景中,將反射中間操作提取出來快取好,下次在使用反射直接從快取中取即可
關掉安全檢查,可以進一步提升效能
最後我們在看一下單次建立物件和單次反射建立物件的耗時,如下圖所示。
Score
表示結果,
Error
表示誤差範圍,在考慮誤差的情況下,它們的耗時差距在
微妙別以內
。
當然根據裝置的不同(高階機、低端機),還有系統、複雜的類等等因素,反射所產生的影響也是不同的。反射在實際專案中應用的非常的廣泛,很多設計和開發都和反射有關,比如透過反射去呼叫位元組碼檔案、呼叫系統隱藏 Api、動態代理的設計模式,Android 逆向、著名的 Spring 框架、各類 Hook 框架等等。
文章中的程式碼已經上傳到 github 倉庫 KtPractice: https://github。com/hi-dhl/KtPractice
全文到這裡就結束了,感謝你的閱讀,如果有幫助,歡迎 在看 、 點贊 、 收藏 、 分享 給身邊的朋友。