美文网首页
通过看字节码指令扒光 Kotlin 内联函数的底裤

通过看字节码指令扒光 Kotlin 内联函数的底裤

作者: BlueSocks | 来源:发表于2023-10-25 18:28 被阅读0次

通过看字节码指令扒光 Kotlin 内联函数的底裤

网上非常多讲 Kotlin 内联函数的文章,绝大部分都是告诉你结论,用了 xxx 关键字就怎么怎么样,过一段时间就又忘了,本篇文章带你从 JVM 字节码来一点一点带你分析它的原理,

普通函数调用

我定义了这样一个类:

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() 函数字节码指令:

 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

我分析一些我自认为有价值的字节码:

4 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>

在入参前会通过 Integer.valueOf() 的静态方法把,int 变量转换成 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

这里会拿到一个 Main$foo1$returnData$1 静态单例对象,其实就是我们的 labmda 表达式对象,然后强制转换成 Function0 对象,然后入栈,然后调用 foo2() 函数,我们仔细看看 foo2() 这个函数的签名,com/tans/test/Main.foo2 : (Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;,传入一个 ObjectFuction0 (在 Kotlin 中他就表示无参数的 lambda),返回值也是 Object,我们明明定义的是传入一个范形,返回也是一个范形,这怎么就用 Object 代替了呢?其实这就是常说的范形擦除,JVM 中的范形是伪范形,运行时方法中的范形都是通过强转来实现的,这也解释了普通方法的范形是不可以通过 T::class 去拿他的 class 对象的,就算拿到的也是 Objectclass 对象。最后返回值会被强制转换成 Number 对象,然后调用其 intValue() 方法完成拆箱操作。

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() 方法的字节码指令:

 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() 方法的字节码指令:

 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 的装箱和拆箱、Kotlinlambda 实现和范形的擦除。

内联函数

普通内联函数

Kotlin 中想要让函数为内联函数添加一个 inline 关键字就好了,我们把上面的 foo2() 函数改造成内联函数:

    // ...
    inline fun <T> foo2(data: T, callback: () -> Unit): T {
        println("Foo2: do something")
        callback()
        return data
    }
    // ...

然后我们再来看看 foo1() 方法的字节码指令:

 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 函数,还是由于别的原因故意为之。

一点一点来看看字节码都做了啥。

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 到控制台。

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 到控制台。

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 到控制台。

以上的字节码等于以下源码:


    // ...
    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() 函数修改成以下:


    // ...
    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 是编译过不了的。

看看字节码:

 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 对象的逻辑,如下:

 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,如以下代码:


    // ...
    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() 函数如下:


    // ...
        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 关键字

我把普通内联函数章节中的代码修改为如下:


    // ...
    inline fun <T> foo2(data: T, noinline callback: () -> Unit): T {
        println("Foo2: do something")
        callback()
        return data
    }
    // ...


这次对应的字节码有改变了,参考以下:

 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 对象,例如以下代码:


    // ...
    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 来返回上一个层级函数的方法,如果内联函数把这个 crossinlinelambda 当参数传递给另外的内联函数当参数,那么这个新调用内联函数中的 lambda 参数也必须是 crossinline 的。读上去有点绕,反正 IDEA 会提示你的。它也会不修改字节码指令。

noinline

在内联函数的 lambda 参数中添加,禁止 lambda 参数内联,当某些情况需要对象化的 lambda 对象时使用,比如一个内联函数中想要把他的 lambda 参数传递给普通的函数时,就需要禁止内联。

相关文章

  • Kotlin 内联函数 inline

    Kotlin 中新增了「内联函数」,内联函数起初是在 C++ 里面的。 那在 Kotlin 中加入内联函数,是有什...

  • Kotlin内联函数

    kotlin内联函数是什么? Kotlin里使用关键字 inline 来表示内联函数。其原理就是:在编译时期,把调...

  • kotlin的内联函数和扩展函数

    kotlin的内联函数和扩展函数 内联函数 https://www.jianshu.com/p/ab877fe72...

  • Kotlin内联函数

    Kotlin里使用关键 inline 来表示内联函数,那么到底什么是内联函数呢,内联函数有什么好处呢? 1. 什么...

  • kotlin-android-extensions 在Fragm

    如果想知道为什么请往下面看,直接上kotlin 字节码 看kotlin源码步骤tools->kotlin->kot...

  • kotlin的内联函数的使用

    kotlin的内联函数属于kotlin的高级特性了,也是不同于java的区别之一;至于为什么kotlin要使用内联...

  • Kotlin实战学习笔记(八 高阶函数)

    1.声明高阶函数 kotlin Java-Kotlin 返回函数的函数 内联函数 // 代码生成到class文件中...

  • Java 字节码指令

    字节码指令链接

  • Kotlin中的函数

    Kotlin中的函数 kotlin中的函数分为普通函数,泛型函数,内联函数,扩展函数,高阶函数以及尾递归函数 1 ...

  • JVM-06

    switch-case的字节码指令: Java代码如下: 字节码指令如下: 结论是:switch-case 语句 ...

网友评论

      本文标题:通过看字节码指令扒光 Kotlin 内联函数的底裤

      本文链接:https://www.haomeiwen.com/subject/irneidtx.html