Exposing Kotlin intrinsic functions by looking at bytecode instructions
There are many articles on the Internet about Kotlin
inline functions. Most of them tell you the conclusion. Just use the xxx keyword and forget about it after a while. This article will guide you. From the JVM
bytecode, I will take you step by step to analyze its principles.
Ordinary function calls
I defined a class like this:
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 } }
The code is very simple. We mainly analyze the functions foo1()
and foo2()
. The tool used to view the bytecode is jclasslib
. Not much nonsense. Say go directly to the bytecode.
foo1()
Function bytecode instructions:
0 iconst_1 1istore_1 2 load_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> 22istore_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
I analyze some bytecodes that I think are valuable:
4 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;>
Before entering the parameters, the int
variable will be converted into an Integer
object through the static method of Integer.valueOf()
. This is what we often say of packing.
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> 22istore_2
Here you will get a Main$foo1$returnData$1
static singleton object, which is actually our labmda
expression object, and then cast it into Function0
object, then push it onto the stack, and then call the foo2()
function. Let’s take a closer look at the signature of foo2()
function, com/tans/test/Main .foo2: (Ljava/lang/Object;Lkotlin/jvm/functions/Function0;)Ljava/lang/Object;
, passing in a Object
and Fuction0
(In Kotlin
, it means lambda
without parameters). The return value is also Object
. What we clearly define is to pass in a paradigm. Return is also a paradigm, so why is it replaced by Object
? In fact, this is what is often called paradigm erasure. The paradigms in JVM
are pseudo-paradigms. The paradigms in runtime methods are all implemented through forced conversion. This also explains the common method. You cannot get its class
object through T::class
, even if you get it, it will be Object
‘s class
object. Finally, the return value will be coerced into a Number
object, and then its intValue()
method will be called to complete the unboxing operation.
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
Here, the Intrinsics#stringPlus
method is used to combine the return values of the "Foo1: returnData="
and foo2()
methods to form a new String
object, and then call the System.out#println()
method to print to the console.
Let’s take a look at the lambda
object mentioned above. Its class name is com/tans/test/Main$foo1$returnData$1
. Let’s take a look at its invoke()
method bytecode instructions:
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
This bytecode instruction is very simple. It loads Callback: do something
directly from the constant pool and then calls the System.out#println()
method to print to the console.
Let’s take a look at the bytecode instructions of the foo2()
method:
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
The method first calls the System.out#println()
method to print Foo2: do something
, and then calls the second parameter Function0#invoke()
method , that is, calling the lambda
object, and finally returning the first input parameter as the return value.
The analysis of bytecode instructions for ordinary function calls is over. From the perspective of bytecode instructions, we looked at the boxing and unboxing of int
and the of
implementation and paradigm erasure.Kotlin
lambda
Inline functions
Ordinary inline functions
In Kotlin
, if you want the function to add an inline
keyword to the inline function, we transform the above foo2()
function into an inline function:
// ... inline fun <T> foo2(data: T, callback: () -> Unit): T {<!-- --> println("Foo2: do something") callback() return data } // ...
Then let’s take a look at the bytecode instructions of the foo1()
method:
0 iconst_1 1 istore_1 2 load_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 load 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 load 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
A brief glance at this bytecode shows that the foo2()
method is not called, and the labmda
object is not constructed. Let’s briefly analyze its process. Due to the The local variable table has become a bit complicated. I will post the contents of its table.
Slot | Value |
---|---|
0 | this(Main object) |
1 | 1 |
2 | – |
3 | this(Main object) |
4 | 1 |
5 | 0 |
6 | “Foo2: do something” |
7 | 0 |
8 | 0 |
9 | “Callback: do something” |
10 | 0 |
I was confused when I first saw this local variable table. I found that many of the objects stored in it were duplicates, and Slot 2
was still empty. I didn’t know whether this was Kotlin
The compiler did not optimize the inline
function, or it was done intentionally for other reasons.
Let’s take a look at what the bytecode does step by step.
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 load 6 22 invokevirtual #43 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
Print Foo2: do something
to the console.
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 load 9 40 invokevirtual #43 <java/io/PrintStream.println : (Ljava/lang/Object;)V>
Print Callback: do something
to the console.
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>
Print Foo1: returnData=
+ 1
to the console.
The above bytecode is equivalent to the following source code:
// ... fun foo1() {<!-- --> val data: Int = 1 println("Foo2: do something") println("Callback: do something") println("Foo1: returnData=$data") } // ...
Based on the above results, we make a summary of the inline function. It will move the bytecode in the inline function directly to the current function for execution, including the instructions in lambda
. Doing this can reduce the creation of method stack frames and lambda
objects at runtime, and can improve program performance under certain conditions; but the disadvantages are also very obvious. If there are many places to call, and at the same time, The logic of the connection function is relatively complex, which will cause the space occupied by class
to increase significantly, because its bytecode will be copied to all called methods.
Add the reified keyword to the template
Let’s just say the conclusion here. If you don’t add the reified
keyword, the compiled bytecode instructions will be exactly the same. Haha, I didn’t expect that after the reified
keyword is marked. The paradigm is a class
object that can directly obtain the paradigm object directly in the inline function.
Then we modify the foo2()
function to the following:
// ... inline fun <reified T> foo2(data: T, callback: () -> Unit): T {<!-- --> valclazz = data!!::class.java println("Foo2: do something") callback() return data } // ...
If there is no reified
keyword here, data!!::class.java
cannot be compiled.
Take a look at the bytecode:
0 iconst_1 1 istore_1 2 load_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 load 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 load 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 load 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
There is no special operation compared to the ordinary inline function above. It just adds the logic of obtaining the class
object, as follows:
5 invokestatic #35 <java/lang/Integer.valueOf : (I)Ljava/lang/Integer;> 8 astore 4 10 iconst_0 11 istore 5 13 load 4 15 invokevirtual #39 <java/lang/Object.getClass : ()Ljava/lang/Class;> 18 astore 6
Directly call the getClass()
method of the Integer
object to obtain the class
object.
At present, it seems that the reified
keyword has no special role in bytecode instructions. I guess that reified
is just a marking function, which may be related to bytecode optimization.
Add crossinline keyword to lambda parameters
Here is a direct conclusion: crossinline
does not modify the bytecode instructions and is the same as the reified
keyword. Surprise or surprise, hahahahaha. So what is it used for? This keyword is used to control return
in lambda
.
Suppose I use return
in the above lambda
, such as the following code:
// ... 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 } // ...
Due to the inline feature, the above method will directly return the foo1()
function. If you want to return lambda
, you have to use return@foo2
; If it is not an inline function, it is forbidden to return the foo1()
function in lambda
of foo2()
.
crossinline
is used to limit the return method of the above example. It cannot return foo1()< in
foo2()
‘s labmda
/code> function.
If I add a foo3()
function as follows:
// ... 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) {<!-- --> } // ...
In fact, the above code cannot be compiled because foo2()
does not add crossinline
, but foo3()
does. crossinline, but foo2()
passes lambda
to foo3()
, then foo2()
The definitions of code> and foo3()
conflict. One allows return
and the other does not allow return
, so foo2()
and foo3()
must both have no crossinline
or both have crossinline
.
Add noinline keyword to lambda parameters
I modified the code in the Ordinary Inline Functions chapter to be as follows:
// ... inline fun <T> foo2(data: T, noinline callback: () -> Unit): T {<!-- --> println("Foo2: do something") callback() return data } // ...
The corresponding bytecode has changed this time, please refer to the following:
0 iconst_1 1istore_1 2 load_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 load 7 30 invokevirtual #50 <java/io/PrintStream.println : (Ljava/lang/Object;)V> 33 load 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
After adding noinline
, the inlining of lambda
will be prohibited, and it will be an object like the ordinary lambda
.
In some cases, we cannot let lambda
be inlined, but need a lambda
object, such as the following code:
// ... inline fun <T> foo2(data: T, callback: () -> Unit): T {<!-- --> foo3(callback) println("Foo2: do something") callback() return data } fun foo3(callback: () -> Unit) {<!-- --> } // ...
The above code cannot be compiled. foo2()
is an inline function, but foo3()
is not. foo2()
will convert lambda
is inlined, and the lambda
required by foo3()
must be an object. In order to pass the compilation, it can be passed in foo2( )
in callback
plus noinline
to prevent it from being inlined, so that foo2()
and foo3()
are all non-inline lambda
objects, so they can be compiled.
Summary
inline
Adding the inline
keyword before a function enables the function to be inlined, including the lambda
parameter. The bytecode instructions will be equivalently moved to the called function. The advantage is that it can improve the running efficiency of the program to a certain extent; the disadvantage is that it will cause the bytecode file to become larger. Use it reasonably.
reified
Added to the paradigm in the inline function, it allows the added paradigm object to directly obtain the Class
object. This keyword will not modify the bytecode instructions.
crossinline
Add in the lambda
parameter of the inline function to disable the inline lambda
to return the method of the previous level function. If the inline function puts this crossinline
‘s lambda
When the parameter is passed to another inline function as a parameter, then the lambda
parameter in the newly called inline function must also be crossinline
of. It’s a bit confusing to read, but IDEA
will prompt you anyway. It also does not modify bytecode instructions.
noinline
Add it to the lambda
parameter of the inline function to disable the lambda
parameter from being inlined. It is used when objectified lambda
objects are needed in certain situations. For example, when you want to pass its lambda
parameter to an ordinary function in an inline function, you need to disable inlining.
Finally
If you want to become an architect or want to break through the 20-30K salary range, then don’t be limited to coding and business, you must be able to select and expand, and improve your programming thinking. In addition, good career planning is also very important, and learning habits are important, but the most important thing is to be able to persevere. Any plan that cannot be implemented consistently is empty talk.
If you have no direction, here is a set of “Advanced Notes on the Eight Modules of Android” written by a senior architect at Alibaba to help you systematically organize messy, scattered, and fragmented knowledge, so that you can systematically and efficiently Master various knowledge points of Android development.
Compared with the fragmented content we usually read, the knowledge points in this note are more systematic, easier to understand and remember, and are strictly arranged according to the knowledge system.
Everyone is welcome to support with one click and three links. If you need the information in the article, just scan the CSDN official certification WeChat card at the end of the article to get it for free ↓↓↓ (There is also a small bonus of the ChatGPT robot at the end of the article, don’t miss it)