美文网首页
通过看字节码指令扒光 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 内联函数的底裤

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