本文永久更新地址: https://xiaozhuanlan.com/topic/3458207169java
重學 Kotlin 已經來到了第三期,前面已經介紹了:web
今天的主角是 inline ,這不是一個 Kotlin 特有的概念,大多數編程語言都支持內聯。編程語言
內聯函數 的語義很簡單: 把函數體複製粘貼到函數調用處 。使用起來也毫無困難,用 inline
關鍵字修飾函數便可。編輯器
然而問題的關鍵並非如何使用 inline
,而是何時使用 inline
? 既然 Kotlin 提供了內聯,它確定是爲了性能優化而存在的,那麼,它又真的是包治百病的性能良藥嗎?ide
今天,咱們就一塊兒來刨根挖底,尋找一下答案。函數
前面已經說過 inline
就是 把函數體複製粘貼到函數調用處 ,徹底是編譯器的小把戲。本着嚴謹科學的態度,咱們仍是來反編譯驗證一下。性能
優化
inline fun test() { println("I'm a inline function") } 複製代碼fun run() { test() } 複製代碼
在 run()
函數中調用了內聯函數 test()
。反編譯查看對應的 java 代碼:
public static final void test() {
String var1 = "I'm a inline function"; System.out.println(var1); } public static final void run() { String var1 = "I'm a inline function"; System.out.println(var1); } 複製代碼
能夠看到 run()
函數中並無直接調用 test()
函數,而是把 test()
函數的代碼直接放入本身的函數體中。這就是 inline
的功效。
那麼,問題就來了。這樣就能夠提升運行效率嗎?若是能夠,爲何?
咱們先從 JVM 的方法執行機制提及。
JVM 進行方法調用和方法執行依賴 棧幀,每個方法從調用開始至執行完成的過程,都對應着一個棧幀在虛擬機棧裏從入棧到出棧的過程。
線程的棧幀是存儲在虛擬機棧中,以上面示例代碼的 未內聯 版本爲例,對應的方法執行過程和對應的棧幀結構以下所示:
未內聯的狀況下,整個執行過程當中會產生兩個方法棧幀,每個方法棧幀都包括了 局部變量表、操做數棧、動態鏈接、方法返回地址和一些額外的附加信息 。
使用內聯的狀況下,只須要一個方法棧幀,下降了方法調用的成本。
乍一看,的確的提升了運行效率,畢竟少用一個棧幀嘛。
然而?
一切看起來都很美好,除了 IDE 給個人刺眼提示。
Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types
大體意思是在這裏使用內聯對性能的影響微乎其微,或者說沒有什麼意義。Kotlin 的內聯最好用在函數參數類型中。
不急着解釋,首先來一發靈魂拷問。
你能夠說不支持,由於 Java 並無提供相似 inline
的顯示聲明內聯函數的方法。
可是 JVM 是支持的。Java 把內聯優化交給虛擬機來進行,從而避免開發者的濫用。
典型的一種濫用, 內聯超長方法 ,極大的增大字節碼長度,反而得不償失。你能夠注意 Kotlin 標準庫中的內聯函數,基本都是簡短的函數。
對於普通的函數調用,JVM 已經提供了足夠的內聯支持。所以,在 Kotlin 中,沒有必要爲普通函數使用內聯,交給 JVM 就好了。
另外,Java 代碼是由 javac
編譯的,Kotlin 代碼是由 kotlinc
編譯的,而 JVM 能夠對字節碼作統一的內聯優化。因此,能夠推斷出,不論是 javac ,仍是 kotlinc,在編譯期是沒有內聯優化的。
至於 JVM 具體的內聯優化機制,我瞭解的並很少,這裏就不作過多介紹了。後續若是我看到相關資料,會到這裏進行補充。
因此,上一節中 IDE 給開發者的提示就很明瞭了。
JVM 已經提供了內聯支持,因此沒有必要在 Kotlin 中內聯普通函數。
那麼問題又來了。 既然 JVM 已經支持內聯優化,Kotlin 的內聯存在的意義是什麼 ? 答案就是 Lambda
。
爲何要拯救 Lambda,咱們首先得知道Kotlin 的 Lambda 對於 JVM 而言到底是什麼。
Kotlin 標準庫中有一個叫 runCatching
的函數,我在這裏實現一個簡化版本 runCatch
,參數是一個函數類型。
fun runCatch(block: () -> Unit){
try { block() }catch (e:Exception){ e.printStackTrace() } } fun run(){ runCatch { println("xxx") } } 複製代碼
反編譯生成的 Java 代碼以下所示:
public final class InlineKt {
public static final void runCatch(@NotNull Function0<Unit> block) { Intrinsics.checkParameterIsNotNull(block, (String)"block"); try { block.invoke(); } catch (Exception e) { e.printStackTrace(); } } public static final void run() { InlineKt.runCatch((Function0<Unit>)((Function0)run.1.INSTANCE)); } } static final class InlineKt.run.1 extends Lambda implements Function0<Unit> { public static final InlineKt.run.1 INSTANCE = new /* invalid duplicate definition of identical inner class */; public final void invoke() { String string = "xxx"; boolean bl = false; System.out.println((Object)string); } InlineKt.run.1() { } } 複製代碼
Kotlin 自誕生之初,就以 兼容 Java 爲首要目標。所以,Kotlin 對於 Lambda 表達式的處理是編譯生成匿名類。
通過編譯器編譯以後, runCatch()
方法中的 Lambda 參數被替換爲 Function0<Unit>
類型,在 run()
方法中實際調用 runCatch()
時傳入的參數是實現了 Function0<Unit>
接口的 InlineKt.run.1
,並重寫 了 invoke()
方法。
因此,調用 runCatch()
的時候,會建立一個額外的類 InlineKt.run.1
。這是 Lambda 沒有捕捉變量的場景。若是捕捉了變量,表現會怎麼樣?
fun runCatch(block: () -> Unit){
try { block() }catch (e:Exception){ e.printStackTrace() } } fun run(){ var message = "xxx" runCatch { println(message) } } 複製代碼
在 Lambda 內部捕捉了外部變量 message
,其對應的 java 代碼以下所示:
public final class InlineKt {
public static final void runCatch(@NotNull Function0<Unit> block) { Intrinsics.checkParameterIsNotNull(block, (String)"block"); try { block.invoke(); } catch (Exception e) { e.printStackTrace(); } } public static final void run() { void message; Ref.ObjectRef objectRef = new Ref.ObjectRef(); objectRef.element = "xxx"; // 這裏每次運行都會 new 一個對象 InlineKt.runCatch((Function0<Unit>)((Function0)new Function0<Unit>((Ref.ObjectRef)message){ final /* synthetic */ Ref.ObjectRef $message; public final void invoke() { String string = (String)this.$message.element; boolean bl = false; System.out.println((Object)string); } { this.$message = objectRef; super(0); } })); } } 複製代碼
若是 Lambda 捕捉了外部變量,那麼每次運行都會 new 一個持有外部變量值的 Function0<Unit>
對象。這比未發生捕捉變量的狀況更加糟糕。
總而言之,Kotlin 的 Lambda 爲了徹底兼容到 Java6,不只增大了編譯代碼的體積,也帶來了額外的運行時開銷。爲了解決這個問題,Kotlin 提供了 inline
關鍵字。
Kotlin 內聯函數的做用是消除 lambda 帶來的額外開銷
給 runCatch()
加持 inline :
inline fun runCatch(block: () -> Unit){
try { block() }catch (e:Exception){ e.printStackTrace() } } fun run(){ var message = "xxx" runCatch { println(message) } } 複製代碼
反編譯查看 java 代碼:
public static final void run() {
Object message = "xxx"; boolean var1 = false; try { int var2 = false; System.out.println(message); } catch (Exception var5) { var5.printStackTrace(); } } 複製代碼
runCatch()
的代碼被直接內聯到 run()
方法中,沒有額外生成其餘類,消除了 Lambda 帶來的額外開銷。
既然 Kotlin 的 Lambda 存在性能問題,那旁邊的 Java 大兄弟確定也逃脫不了。
從 Java8 開始,Java 藉助 invokedynamic
來完成的 Lambda 的優化。
invokedynamic
用於支持動態語言調用。在首次調用時,它會生成一個調用點,並綁定該調用點對應的方法句柄。後續調用時,直接運行該調用點對應的方法句柄便可。說直白一點,第一次調用 invokeddynamic
時,會找到此處應該運行的方法並綁定, 後續運行時就直接告訴你這裏應該執行哪一個方法。
關於 invokedynamic
的詳細介紹,能夠閱讀極客時間專欄 《深刻拆解Java虛擬機》的第 8,9 兩講。
一個高階函數一旦被標記爲內聯,它的方法體和全部 Lambda 參數都會被內聯。
inline fun test(block1: () -> Unit, block2: () -> Unit) {
block1() println("xxx") block2() } 複製代碼
test()
函數被標記爲了 inline
,因此它的函數體以及兩個 Lambda 參數都會被內聯。可是因爲我要傳入的 block1
代碼塊巨長(或者其餘緣由),我並不想將其內聯,這時候就要使用 noinline
。
inline fun test(noinline block1: () -> Unit, block2: () -> Unit) {
block1() println("xxx") block2() } 複製代碼
這樣, block1
就不會被內聯了。篇幅緣由,這裏就不展現 Java 代碼了,相信你也能很容易理解 noinline
。
首先,普通的 lambda 是不容許直接使用 return
的 。
fun runCatch(block: () -> Unit) {
try { print("before lambda") block() print("after lambda") } catch (e: Exception) { e.printStackTrace() } } fun run() { // 普通 lambda 不容許 return runCatch { return } } 複製代碼
上面的代碼沒有辦法經過編譯,IDE 會提示你 return is not allowed here 。 而 inline
可讓咱們突破這個限制。
// 標記爲 inline
inline fun runCatch(block: () -> Unit) { try { print("before lambda") block() print("after lambda") } catch (e: Exception) { e.printStackTrace() } } fun run() { runCatch { return } } 複製代碼
上面的代碼是能夠正常編譯運行的。和以前的例子惟一的區別就是多了 inline
。
既然容許 return
,那麼這裏到底是從 Lambda 中返回,繼續運行後面的代碼?仍是直接結束外層函數的運行呢?看一下 run()
方法的執行結果。
before lambda
複製代碼
從運行結果來看,是直接結束外層函數的運行。其實不難理解,這個 return 是直接內聯到 run()
方法內部的,至關於在 run()
方法中直接調用 return
。從反編譯的 java 代碼看,一目瞭然。
public static final void run() {
boolean var0 = false; try { String var1 = "before lambda"; System.out.print(var1); int var2 = false; } catch (Exception var3) { var3.printStackTrace(); } } 複製代碼
編譯器直接把 return
以後的代碼優化掉了。這樣的場景叫作 non-local return (非局部返回) 。
可是有些時候我並不想直接退出外層函數,而是僅僅退出 Lambda 的運行,就能夠這樣寫。
inline fun runCatch(block: () -> Unit) {
try { print("before lambda") block() print("after lambda") } catch (e: Exception) { e.printStackTrace() } } fun run() { // 從 lambda 中返回 runCatch { return@runCatch } } 複製代碼
return@label
,這樣就會繼續執行 Lambda 以後的代碼了。這樣的場景叫作 局部返回 。
還有一種場景,我是 API 的設計者,我不想 API 使用者進行非局部返回 ,改變個人代碼流程。同時我又想使用 inline ,這樣實際上是衝突的。前面介紹過,內聯會讓 Lambda 容許非局部返回。
crossinline
就是爲了解決這一衝突而生。它能夠在保持內聯的狀況下,禁止 lambda 從外層函數直接返回。
inline fun runCatch(crossinline block: () -> Unit) {
try { print("before lambda") block() print("after lambda") } catch (e: Exception) { e.printStackTrace() } } fun run() { runCatch { return } } 複製代碼
添加 crossinline
以後,上面的代碼將沒法編譯。但下面的代碼仍然是能夠編譯運行的。
inline fun runCatch(crossinline block: () -> Unit) {
try { print("before lambda") block() print("after lambda") } catch (e: Exception) { e.printStackTrace() } } fun run() { runCatch { return@runCatch } } 複製代碼
crossinline
能夠阻止非局部返回,但並不能阻止局部返回,其實也沒有必要。
關於內聯函數,一口氣說了這麼多,總結一下。
在 Kotlin 中,內聯函數是用來彌補高階函數中 Lambda 帶來的額外運行開銷的。對於普通函數,沒有必要使用內聯,由於 JVM 已經提供了必定的內聯支持。
對指定的 Lambda 參數使用 noinline
,能夠避免該 Lambda 被內聯。
普通的 Lambda 不支持非局部返回,內聯以後容許非局部返回。既要內聯,又要禁止非局部返回,請使用 crossinline
。
除了內聯函數以外,Kotlin 1.3 開始支持 inline class ,但這是一個實驗性 API,須要手動開啓編譯器支持。不知道你們對內聯類有什麼獨特的見解,歡迎在評論區交流。
本文使用 mdnice 排版
這裏是秉心說,關注我,不迷路!