1. 背景
在JAVA8中,Lamdba表達式通過invokedynamic指令實現的,通過invokedynamic可以避免編譯期硬編碼生成內部匿名類的實現,而是由JIT在運行時才產生相應的接入點代碼,顯著減少靜態生成的類和字節碼大小。
由於Kotlin支持JAVA6,Kotlin對於Lamdba表達式的支持不得不通過編譯期硬編碼的方式實現,導致大量的內部匿名類,爲了避免此問題,Kotlin引入inline、noinline、crossinline等關鍵字,以減少額外生成的匿名類數以及函數執行的時間開銷。
本文將分析inline、noinline、crossinline關鍵字的字節碼實現。
2. inline關鍵字
fun main() {
fun3{
println("lambda1")
}
}
fun fun3(funparam: () -> Unit) {
println("fun1 starting")
funparam()
println("fun1 end")
}
上文是一個簡單的Lamdba的調用,正如文中所描述的,編譯器會針對Lamdba中的邏輯自動生成一個內部匿名類,外部代碼通過調用此內部匿名類的invoke方法執行Lamdba邏輯。
通過下面的例程,我們看看Kotlin的內聯是如何操作的。
fun main() {
fun4{
println("lambda1")
}
}
inline fun fun4(funparam: () -> Unit) {
println("fun1 starting")
funparam()
println("fun1 end")
}
執行結果如下:
fun1 starting
lambda1
fun1 end
反編譯字節碼如下:
public final class T4Kt {
public static final void main();
Code:
0: iconst_0
1: istore_0
2: ldc #11 // String fun1 starting
4: astore_1
5: iconst_0
6: istore_2
7: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
10: aload_1
11: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
14: iconst_0
15: istore_3
16: ldc #25 // String lambda1
18: astore 4
20: iconst_0
21: istore 5
23: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
26: aload 4
28: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
31: nop
32: ldc #27 // String fun1 end
34: astore_1
35: iconst_0
36: istore_2
37: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
40: aload_1
41: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
44: nop
45: return
public static void main(java.lang.String[]);
Code:
0: invokestatic #9 // Method main:()V
3: return
}
使用inline關鍵字修飾fun4後,編譯結果中沒有了內部匿名類,Lambda表達式邏輯、fun4就均被內聯到public static final void main()方法中 ,程序佔用資源減少並且執行效率有提升。
3. 非局部返回
由於方法內聯後,內聯函數函數體和Lambda邏輯都會直接替代具體的調用,如果Lambda邏輯中包含return語句,則導致非局部返回。
fun main() {
fun5{
println("lambda1")
return
}
}
inline fun fun5(funparam: () -> Unit) {
println("fun1 starting")
funparam()
println("fun1 end")
}
執行結果如下:
fun1 starting
lambda1
我們注意到,最後的"fun1 end"預計沒有被執行。
public static final void main();
Code:
0: iconst_0
1: istore_0
2: ldc #11 // String fun1 starting
4: astore_1
5: iconst_0
6: istore_2
7: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
10: aload_1
11: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
14: iconst_0
15: istore_3
16: ldc #25 // String lambda1
18: astore 4
20: iconst_0
21: istore 5
23: getstatic #17 // Field java/lang/System.out:Ljava/io/PrintStream;
26: aload 4
28: invokevirtual #23 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
31: return
public static void main(java.lang.String[]);
Code:
0: invokestatic #9 // Method main:()V
3: return
從字節碼中也可以看出println(“fun1 end”)語句根本就沒有被執行。
非局部返回尤其在循環控制中顯得特別有用,比如Kotlin的forEach接口,它接收的就是一個Lambda參數,由於它也是一個內聯函數,所以可以直接在它調用的Lambda中執行return退出上一層的程序。
4. crossinline
雖然某些場景下非局部返回可能非常有用,但還是可能存在危險,我們可以通過crossline關鍵字防止非局部返回的發生。
fun main() {
fun5{
println("lambda1")
return
}
}
inline fun fun5(crossinline funparam: () -> Unit) {
println("fun1 starting")
funparam()
println("fun1 end")
}
執行結果是’return’ is not allowed here
5. 總結
出於Kotlin語言定位的考量,Kotlin當前採用方法內聯對Lambda帶來的額外開銷進行優化,而不是JAVA7引入的invokedynamic。
Kotlin的方法內聯是編譯器硬編碼的方式,與JIT的運行動態的方法內聯不同,也是不同層次的優化,應當注意區分。