Kotlin 反射你敢用嗎?

  • 2020 年 2 月 20 日
  • 筆記

其實一直想寫一篇詳細介紹 Kotlin 反射的文章,但問題就在於,現階段的 Kotlin 反射還真不如直接用 Java 反射來的愉快。

你問我原因?那我們就來簡單說說。

本文結論基於 Kotlin 1.1.51,相信在未來的版本,本文提到的問題都將被一一解決。

1 一個 2.5M 大小的 jar 包

Java 反射直接內置在 Java 標準庫當中,而 Kotlin 的反射需要單獨引入,原因也很簡單,Kotlin 反射庫居然有 2.5M。如果不混淆,這 2.5M 的 class 文件都將最終打包到你的應用程序中,如果恰好你的程序對於體積還比較敏感,那麼這將是一個值得考慮的問題。比如 Android,我們做個簡單的實驗,引用了反射庫編譯 debug 包之後大小如下:

如果不帶反射包,如下:

我們看到 classes.dex 的下載大小前後差到 0.6M,當然混淆了之後會小一些。

我們再看下同樣的工程,在簡單的引用了某一個類的 member 之後(這樣做目的是混淆之後反射包不至於被剔除掉)進行混淆,包含反射包的大小是多少:

不包含反射包的混淆後大小為:

儘管混淆之後整體體積會小一些,但 Kotlin 的反射包造成的影響卻不會小很多。

2 不支持的 built-in Kotlin types

如果你嘗試用 Kotlin 反射訪問下 String,你會發現 Boom,你的代碼 crash 了。這是怎麼回事呢?

String::class.memberFunctions

錯誤信息很明確的說了,內置的 Kotlin 類型暫時沒有被完全支持。

Exception in thread "main" kotlin.reflect.jvm.internal.KotlinReflectionInternalError:    Reflection on built-in Kotlin types is not yet fully supported.    No metadata found for public open val length:  kotlin.Int defined in kotlin.String[DeserializedPropertyDescriptor@1018bde2]
  • 那麼什麼是 Kotlin 內置的類型呢? 在 Kotlin 當中,存在不少並非真實存在,而是編譯期映射的類型,例如 kotlin.Int 等數值類型,實際上是映射到了 Java 虛擬機類型中的基本類型和裝箱類型; kotlin.collections.Set 等集合類型,或通過編譯實現映射,或直接通過類型別名映射,也都對應到了 Java 虛擬機類型中的集合框架。這樣的類型,我們就可以認為是 Kotlin 的內置類型。
  • 那麼既然是不完全支持,那麼哪些類型有上述問題呢? StringMapSetArray 等這些類都會觸發上述問題。
  • 如何針對這些類使用反射呢? 考慮到這些類比較特殊,都是 Java 的原生類型,在 Kotlin 反射尚不能完全支持之前,建議使用 Java 反射。

3 還沒來得及優化的性能

曾經在 Kotlin 的官方論壇上面看到有開發者抱怨 Kotlin 反射 API 耗時比 Java 反射長,官方開發者給出的答覆是:目前在 Kotlin 反射框架上還沒有花太多精力進行性能優化。

那麼 Kotlin 反射究竟有多慢呢?我們對比下 Java 反射和 Kotlin 反射訪問屬性、修改屬性、調用方法、構造對象以及前面提到的獲取泛型參數的例子的耗時情況,如下(僅供參考):

單位:微秒 μs

構造對象

訪問屬性

修改屬性

調用方法

Java 反射

12.7

25.2

12.2

18.8

Kotlin 反射

14938.0

85247.5

1316.7

326.3

以上數據在我的 MacBook Pro 上面運行所得,結果因設備有差異,但我們可以看到,Java 反射基本在 μs 級別,而 Kotlin 反射基本耗時在 ms 級別。

4 小結

整體來看,Kotlin 反射仍處於一個不太成熟的階段,無論從體積還是從性能上考慮,現階段使用 Kotlin 反射都應該保持一種謹慎的態度。當然,這不應該成為你排斥 Kotlin 的理由,畢竟 Kotlin 標準庫已經非常成熟,並且絕大多數開發者是用不到反射的。