通过看字节码指令扒光 Kotlin 内联函数的底裤
网上非常多讲 Kotlin
内联函数的文章,绝大部分都是告诉你结论,用了 xxx 关键字就怎么怎么样,过一段时间就又忘了,本篇文章带你从 JVM
字节码来一点一点带你分析它的原理,在开始之前你需要有基础的 JVM
字节码指令的相关知识,如果没有可以参考我之前的文章 JVM 字节码。如果有忘记了的字节码指令可以查看这个文档 字节码指令
普通函数调用
我定义了这样一个类:
Kotlin
package com.tans.test
object Main {
@JvmStatic
fun main(args: Array<String>) {
foo1()
}
fun foo1() {
val data: Int = 1
val returnData = foo2(data) {
println("Callback: do something")
}
println("Foo1: returnData=$returnData")
}
fun <T> foo2(data: T, callback: () -> Unit): T {
println("Foo2: do something")
callback()
return data
}
}
代码很简单,我们主要分析函数 foo1()
和 foo2()
,查看字节码用的工具是 jclasslib
,废话不多说直接上字节码。
foo1()
函数字节码指令:
less
0 iconst_1
1 istore_1
2 aload_0
3 iload_1
4 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
7 getstatic #40 <com/tans/test/Main$foo1$returnData$1.INSTANCE : Lcom/tans/test/Main$foo1$returnData$1;>
10 checkcast #42 <kotlin/jvm/functions/Function0>
13 invokevirtual #46 <com/tans/test/Main.foo2 : (Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;>
16 checkcast #48 <java/lang/Number>
19 invokevirtual #52 <java/lang/Number.intValue : ()I>
22 istore_2
23 ldc #54 <Foo1: returnData=>
25 iload_2
26 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
29 invokestatic #58 <kotlin/jvm/internal/Intrinsics.stringPlus : (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;>
32 astore_3
33 iconst_0
34 istore 4
36 getstatic #64 <java/lang/System.out : Ljava/io/PrintStream;>
39 aload_3
40 invokevirtual #70 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
43 return
我分析一些我自认为有价值的字节码:
vbnet
4 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
在入参前会通过 Integer.valueOf()
的静态方法把,int
变量转换成 Integer
对象,这也就是我们常说的装箱。
less
7 getstatic #40 <com/tans/test/Main$foo1$returnData$1.INSTANCE : Lcom/tans/test/Main$foo1$returnData$1;>
10 checkcast #42 <kotlin/jvm/functions/Function0>
13 invokevirtual #46 <com/tans/test/Main.foo2 : (Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;>
16 checkcast #48 <java/lang/Number>
19 invokevirtual #52 <java/lang/Number.intValue : ()I>
22 istore_2
这里会拿到一个 Main$foo1$returnData$1
静态单例对象,其实就是我们的 labmda
表达式对象,然后强制转换成 Function0
对象,然后入栈,然后调用 foo2()
函数,我们仔细看看 foo2()
这个函数的签名,com/tans/test/Main.foo2 : (Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
,传入一个 Object
和 Fuction0
(在 Kotlin
中他就表示无参数的 lambda
),返回值也是 Object
,我们明明定义的是传入一个范形,返回也是一个范形,这怎么就用 Object
代替了呢?其实这就是常说的范形擦除,JVM
中的范形是伪范形,运行时方法中的范形都是通过强转来实现的,这也解释了普通方法的范形是不可以通过 T::class
去拿他的 class
对象的,就算拿到的也是 Object
的 class
对象。最后返回值会被强制转换成 Number
对象,然后调用其 intValue()
方法完成拆箱操作。
vbnet
23 ldc #54 <Foo1: returnData=>
25 iload_2
26 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
29 invokestatic #58 <kotlin/jvm/internal/Intrinsics.stringPlus : (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;>
32 astore_3
33 iconst_0
34 istore 4
36 getstatic #64 <java/lang/System.out : Ljava/io/PrintStream;>
39 aload_3
40 invokevirtual #70 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
43 return
这里就是通过 Intrinsics#stringPlus
方法组合 "Foo1: returnData="
和 foo2()
方法的返回值构成一个新的 String
对象,然后调用 System.out#println()
方法打印到控制台。
我们再看看上面说到的 lambda
对象,他的类名是 com/tans/test/Main$foo1$returnData$1
,我们来看看他的 invoke()
方法的字节码指令:
bash
0 ldc #17 <Callback: do something>
2 astore_1
3 iconst_0
4 istore_2
5 getstatic #23 <java/lang/System.out : Ljava/io/PrintStream;>
8 aload_1
9 invokevirtual #29 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
12 return
这个字节码指令非常简单,直接从常量池中加载 Callback: do something
然后调用 System.out#println()
方法打印到控制台。
我们再来看看 foo2()
方法的字节码指令:
less
0 aload_2
1 ldc #76 <callback>
3 invokestatic #22 <kotlin/jvm/internal/Intrinsics.checkNotNullParameter : (Ljava/lang/Object;Ljava/lang/String;)V>
6 ldc #78 <Foo2: do something>
8 astore_3
9 iconst_0
10 istore 4
12 getstatic #64 <java/lang/System.out : Ljava/io/PrintStream;>
15 aload_3
16 invokevirtual #70 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
19 aload_2
20 invokeinterface #82 <kotlin/jvm/functions/Function0.invoke : ()Ljava/lang/Object;> count 1
25 pop
26 aload_1
27 areturn
方法首先调用 System.out#println()
方法打印 Foo2: do something
,然后调用第二个参数 Function0#invoke()
方法,也就是调用 lambda
对象,最后将输入的第一个参数当返回值返回。
普通函数调用的字节码指令分析就结束了,我们从字节码指令的角度看了 int
的装箱和拆箱、Kotlin
的 lambda
实现和范形的擦除。
内联函数
普通内联函数
在 Kotlin
中想要让函数为内联函数添加一个 inline
关键字就好了,我们把上面的 foo2()
函数改造成内联函数:
Kotlin
// ...
inline fun <T> foo2(data: T, callback: () -> Unit): T {
println("Foo2: do something")
callback()
return data
}
// ...
然后我们再来看看 foo1()
方法的字节码指令:
bash
0 iconst_1
1 istore_1
2 aload_0
3 astore_3
4 iload_1
5 istore 4
7 iconst_0
8 istore 5
10 ldc #31 <Foo2: do something>
12 astore 6
14 iconst_0
15 istore 7
17 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;>
20 aload 6
22 invokevirtual #43 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
25 iconst_0
26 istore 8
28 ldc #45 <Callback: do something>
30 astore 9
32 iconst_0
33 istore 10
35 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;>
38 aload 9
40 invokevirtual #43 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
43 nop
44 iload 4
46 istore_2
47 ldc #47 <Foo1: returnData=>
49 iload_2
50 invokestatic #53 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
53 invokestatic #57 <kotlin/jvm/internal/Intrinsics.stringPlus : (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;>
56 astore_3
57 iconst_0
58 istore 4
60 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;>
63 aload_3
64 invokevirtual #43 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
67 return
简单浏览一下这个字节码,其中没有调用 foo2()
方法了,也没有构建 labmda
对象了,我们再来简单分析他的流程,由于该方法的本地变量表变得有些复杂,我把它的表中的内容贴一下。
Slot | 值 |
---|---|
0 | this(Main 对象) |
1 | 1 |
2 | - |
3 | this(Main 对象) |
4 | 1 |
5 | 0 |
6 | "Foo2: do something" |
7 | 0 |
8 | 0 |
9 | "Callback: do something" |
10 | 0 |
我刚开始看到这个本地变量表的时候也懵了,发现其中存放了很多对象都是重复的,而且 Slot 2
还是空的,我不清楚这是 Kotlin
编译器没有优化好 inline
函数,还是由于别的原因故意为之。
一点一点来看看字节码都做了啥。
bash
10 ldc #31 <Foo2: do something>
12 astore 6
14 iconst_0
15 istore 7
17 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;>
20 aload 6
22 invokevirtual #43 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
打印 Foo2: do something
到控制台。
bash
25 iconst_0
26 istore 8
28 ldc #45 <Callback: do something>
30 astore 9
32 iconst_0
33 istore 10
35 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;>
38 aload 9
40 invokevirtual #43 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
打印 Callback: do something
到控制台。
vbnet
47 ldc #47 <Foo1: returnData=>
49 iload_2
50 invokestatic #53 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
53 invokestatic #57 <kotlin/jvm/internal/Intrinsics.stringPlus : (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;>
56 astore_3
57 iconst_0
58 istore 4
60 getstatic #37 <java/lang/System.out : Ljava/io/PrintStream;>
63 aload_3
64 invokevirtual #43 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
打印 Foo1: returnData=
+ 1
到控制台。
以上的字节码等于以下源码:
Kotlin
// ...
fun foo1() {
val data: Int = 1
println("Foo2: do something")
println("Callback: do something")
println("Foo1: returnData=$data")
}
// ...
根据以上结果对内联函数做一个总结,他会把内联函数中的字节码直接移动到当前函数中执行,也包括 lambda
中的指令。这样做可以在运行时减少方法栈帧的创建,减少 lambda
对象的创建,在一定条件下可以提高程序性能;但是坏处也非常明显,如果调用的地方非常多,同时内联函数的逻辑比较复杂,那它会导致 class
所占用的空间明显增大,因为他的字节码要被复制到所有的调用的方法中。
为范形添加 reified 关键字
这里直接说结论添加不添加 reified
关键字,编译出来的字节码指令都是完全一样的,哈哈,我也没想到,reified
关键字标记后的范形是可以直接在内联函数中直接获取范形对象的 class
对象。
那我们把 foo2()
函数修改成以下:
Kotlin
// ...
inline fun <reified T> foo2(data: T, callback: () -> Unit): T {
val clazz = data!!::class.java
println("Foo2: do something")
callback()
return data
}
// ...
这里如果没有 reified
关键字, data!!::class.java
是编译过不了的。
看看字节码:
less
0 iconst_1
1 istore_1
2 aload_0
3 astore_3
4 iload_1
5 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
8 astore 4
10 iconst_0
11 istore 5
13 aload 4
15 invokevirtual #39 <java/lang/Object.getClass : ()Ljava/lang/Class;>
18 astore 6
20 ldc #41 <Foo2: do something>
22 astore 7
24 iconst_0
25 istore 8
27 getstatic #47 <java/lang/System.out : Ljava/io/PrintStream;>
30 aload 7
32 invokevirtual #53 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
35 iconst_0
36 istore 9
38 ldc #55 <Callback: do something>
40 astore 10
42 iconst_0
43 istore 11
45 getstatic #47 <java/lang/System.out : Ljava/io/PrintStream;>
48 aload 10
50 invokevirtual #53 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
53 nop
54 aload 4
56 checkcast #57 <java/lang/Number>
59 invokevirtual #61 <java/lang/Number.intValue : ()I>
62 istore_2
63 ldc #63 <Foo1: returnData=>
65 iload_2
66 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
69 invokestatic #67 <kotlin/jvm/internal/Intrinsics.stringPlus : (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;>
72 astore_3
73 iconst_0
74 istore 4
76 getstatic #47 <java/lang/System.out : Ljava/io/PrintStream;>
79 aload_3
80 invokevirtual #53 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
83 return
和上面普通的内联函数没有什么特殊的操作,只是增加了获取 class
对象的逻辑,如下:
vbnet
5 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
8 astore 4
10 iconst_0
11 istore 5
13 aload 4
15 invokevirtual #39 <java/lang/Object.getClass : ()Ljava/lang/Class;>
18 astore 6
直接调用 Integer
对象的 getClass()
方法获取 class
对象。
目前看上去 reified
关键字在字节码指令中没有什么特殊作用,我猜测 reified
只是一个标记作用,可能和字节码优化有关。
为 lambda 参数添加 crossinline 关键字
这里直接给结论 crossinline
是不会修改字节码指令和 reified
关键字一样,惊不惊喜,意不意外,哈哈哈哈哈。那它是用来干嘛的呢?这个关键字是用来控制 lambda
中的 return
的。
假如我在上面的 lambda
中用了 return
,如以下代码:
Kotlin
// ...
fun foo1() {
val data: Int = 1
val returnData = foo2(data) {
println("Callback: do something")
return
}
println("Foo1: returnData=$returnData")
}
inline fun <T> foo2(data: T, callback: () -> Unit): T {
println("Foo2: do something")
callback()
return data
}
// ...
由于内联的特性上面的方法就会直接返回 foo1()
函数,如果是要返回 lambda
就得这么用 return@foo2
;如果不是内联函数是禁止在 foo2()
的 lambda
中返回 foo1()
函数。
crossinline
它就是用来限制上面例子的返回方式,不能够在 foo2()
的 labmda
中返回 foo1()
函数。
假如我加了一个 foo3()
函数如下:
Kotlin
// ...
fun foo1() {
val data: Int = 1
val returnData = foo2(data) {
println("Callback: do something")
}
println("Foo1: returnData=$returnData")
}
inline fun <T> foo2(data: T, callback: () -> Unit): T {
foo3(callback)
println("Foo2: do something")
callback()
return data
}
inline fun foo3(crossinline callback: () -> Unit) {
}
// ...
其实上面的代码是编译无法通过的,因为 foo2()
中没有添加 crossinline
,而 foo3()
中有添加 crossinline
,但是 foo2()
又把 lambda
传给 foo3()
了,那 foo2()
和 foo3()
的定义就冲突了,一个允许 return
,一个不允许 return
,所以 foo2()
和 foo3()
必须同时没有 crossinline
或者同时有 crossinline
。
为 lambda 参数添加 noinline 关键字
我把普通内联函数章节中的代码修改为如下:
Kotlin
// ...
inline fun <T> foo2(data: T, noinline callback: () -> Unit): T {
println("Foo2: do something")
callback()
return data
}
// ...
这次对应的字节码有改变了,参考以下:
less
0 iconst_1
1 istore_1
2 aload_0
3 astore_3
4 iload_1
5 istore 4
7 getstatic #34 <com/tans/test/Main$foo1$returnData$1.INSTANCE : Lcom/tans/test/Main$foo1$returnData$1;>
10 checkcast #36 <kotlin/jvm/functions/Function0>
13 astore 5
15 iconst_0
16 istore 6
18 ldc #38 <Foo2: do something>
20 astore 7
22 iconst_0
23 istore 8
25 getstatic #44 <java/lang/System.out : Ljava/io/PrintStream;>
28 aload 7
30 invokevirtual #50 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
33 aload 5
35 invokeinterface #54 <kotlin/jvm/functions/Function0.invoke : ()Ljava/lang/Object;> count 1
40 pop
41 iload 4
43 istore_2
44 ldc #56 <Foo1: returnData=>
46 iload_2
47 invokestatic #62 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
50 invokestatic #66 <kotlin/jvm/internal/Intrinsics.stringPlus : (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String;>
53 astore_3
54 iconst_0
55 istore 4
57 getstatic #44 <java/lang/System.out : Ljava/io/PrintStream;>
60 aload_3
61 invokevirtual #50 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
64 return
加了 noinline
以后会禁止 lambda
内联化,和普通的 lambda
一样会是一个对象。
在某些情况下,我们不能让 lambda
内联化,而是需要一个 lambda
对象,例如以下代码:
Kotlin
// ...
inline fun <T> foo2(data: T, callback: () -> Unit): T {
foo3(callback)
println("Foo2: do something")
callback()
return data
}
fun foo3(callback: () -> Unit) {
}
// ...
以上代码是无法编译的,foo2()
是一个内联函数,而 foo3()
不是,foo2()
会把 lambda
内联化,而 foo3()
需要的 lambda
必须是一个对象,为了编译能够给通过就可以通过在 foo2()
中的 callback
加上 noinline
来阻止其内联化,这样 foo2()
和 foo3()
都是非内联的 lambda
对象,这样就可以通过编译了。
总结
inline
函数前加 inline
关键字能让函数内联化,包括其中的 lambda
参数,其中的字节码指令会等效地移动到调用的函数中。优点是一定程度上能够提升程序的运行效率;缺点是会导致字节码文件变大。要合理使用。
reified
在内联函数中的范形中添加,它可以让添加后的范形对象能够直接拿到 Class
对象,这个关键字不会修改字节码指令。
crossinline
在内联函数的 lambda
参数中添加,禁止内联后的 lambda
来返回上一个层级函数的方法,如果内联函数把这个 crossinline
的 lambda
当参数传递给另外的内联函数当参数,那么这个新调用内联函数中的 lambda
参数也必须是 crossinline
的。读上去有点绕,反正 IDEA
会提示你的。它也会不修改字节码指令。
noinline
在内联函数的 lambda
参数中添加,禁止 lambda
参数内联,当某些情况需要对象化的 lambda
对象时使用,比如一个内联函数中想要把他的 lambda
参数传递给普通的函数时,就需要禁止内联。