Kotlin 内联函数(inline)

102 阅读6分钟

Kotlin 内联函数(inline)

1.Lambda表达式

下面是一个带有Lamdba表达式的方法

fun test(a:()->Unit){
    println("a执行前打印")
    a.invoke()
}

另一种写法:

fun test(a:()->Unit){
    println("a执行前打印")
    a()
}

对于a.invoke()和a(),在这里作用相同。都是要执行参数所包裹的方法体。

运行代码:

fun main(){
    test {
        println("test 方法体")
    }
}

执行结果:

a执行前打印
test 方法体

在上面运行代码中你看到的test()方法的调用方式就是Lambda表达式。
你可以理解成这样:a.invoke() -> {println("test 方法体")}。在kotlin中a:() -> Unit参数你可以理解为代码块。但是它不是真的代码块,虽然比较拗口,你可以接着向下看。下面2.12.1内联函数“复制”代码块会详细讲解。

2.内联函数

2.1 内联函数“复制”代码块

示例代码:

fun main(){
    test()
}

fun test(){
    println("a执行前打印")
    println("打印第二条")
}

反编译为kotlin为java字节码

public final class KotlinDemoKt {
   public static final void main() {
      test();
   }

   public static void main(String[] var0) {
      main();
   }

   public static final void test() {
      String var0 = "a执行前打印";
      System.out.println(var0);
      var0 = "打印第二条";
      System.out.println(var0);
   }
}

这种执行方式是kotlin代码常规调用方式,代码运行层级main(String[] var0) -> main() -> test(),按照这样的顺序进行执行。

今天主角要出场了,看下面的代码

fun main(){
    test()
}


inline fun test(){
    println("a执行前打印")
    println("打印第二条")
}

我们来看下java字节码

public final class KotlinDemoKt {
   public static final void main() {
      int $i$f$test = false;
      String var1 = "a执行前打印";
      System.out.println(var1);
      var1 = "打印第二条";
      System.out.println(var1);
   }
   
   public static void main(String[] var0) {
      main();
   }

   public static final void test() {
      int $i$f$test = 0;
      String var1 = "a执行前打印";
      System.out.println(var1);
      var1 = "打印第二条";
      System.out.println(var1);
   }
}

仔细观察上面编译成java字节码的方法调用层级:main(String[] var0) -> main()
可以看到在test()方法上添加了一个inline关键字后,test()方法中的内容被“复制”到了test()被使用的方法体内,这就是inline的作用。有一定的优化作用,但是貌似作用也不是很大。
结合Lambda表达式你就会发现它的妙处。你还记得上面的问题吗?Lambda是怎么执行的哪?

fun main() {
    test{
        println("test 方法体")
    }
}


fun test(a:()->Unit){
    println("a执行前打印")
    a.invoke()   // 执行方法体 println("test 方法体")
}

上面代码是1.Lambda表达式中的示例代码。接下来打印下它的java字节码。

public final class KotlinDemoKt {
   public static final void main() {
      test((Function0)null.INSTANCE);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }

   public static final void test(@NotNull Function0 a) {
      Intrinsics.checkNotNullParameter(a, "a");
      String var1 = "a执行前打印";
      System.out.println(var1);
          a.invoke();
   }
}

代码执行层级 main(String[] var0) -> main() -> test(@NotNull Function0 a)
从上面test(@NotNull Function0 a)可以看到,Lambda表达式在编译时会当做一个对象Function0作为参数。也就是说咱们的Lambda表达式最终是以对象形式调用。虽然相对来说创建一个函数没什么大不了,但是如果下面这个结构又该怎么办?

fun main() {
    for(i in 1..10){
        test{
            println("test 方法体 $i")
        }  
    }
 
}


fun test(a:()->Unit){
    println("a执行前打印")
    a.invoke()   // 执行方法体 println("test 方法体")
}

从kotlin字节码中可以看到,Lambda表达式每次都会帮我们创建一个对象处理方法体。上面这个例子中遍历1-10就会创建10次对象。在代码中遍历更多次的情况很常见,所以此时内联函数就非常有必要了。

public final class KotlinDemoKt {
   public static final void main() {
      final int i = 1;

      for(byte var1 = 10; i <= var1; ++i) {
         test((Function0)(new Function0() {
            public Object invoke() {
               this.invoke();
               return Unit.INSTANCE;
            }

            public final void invoke() {
               String var1 = "test 方法体 " + i;
               System.out.println(var1);
            }
         }));
      }

   }

   public static void main(String[] var0) {
      main();
   }

   public static final void test(@NotNull Function0 a) {
      Intrinsics.checkNotNullParameter(a, "a");
      String var1 = "a执行前打印";
      System.out.println(var1);
      a.invoke();
   }

上面示例字节码。和我们想的一样,创建十次Function0对象。

使用inline函数关键字后的源码:

fun main() {
    for(i in 1..10){
        test{
            println("test 方法体 $i")
        }
    }

}


inline fun test(a:()->Unit){
    println("a执行前打印")
    a.invoke()   // 执行方法体 println("test 方法体")
}

接下来看下它对应的java字节码:

public final class KotlinDemoKt {
   public static final void main() {
      int i = 1;

      for(byte var1 = 10; i <= var1; ++i) {
         int $i$f$test = false;
         String var3 = "a执行前打印";
         System.out.println(var3);
         int var4 = false;
         String var5 = "test 方法体 " + i;
         System.out.println(var5);
      }

   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }

   public static final void test(@NotNull Function0 a) {
      int $i$f$test = 0;
      Intrinsics.checkNotNullParameter(a, "a");
      String var2 = "a执行前打印";
      System.out.println(var2);
      a.invoke();
   }
}

可以看到,使用inline关键字便会将内联函数中的方法体“复制”到被执行的方法体内。对于上述类似for循环这样的性能问题就可以进行优化。

3.noinline

这个关键字名称很好理解,不内联。
下面是使用了noinline关键字的kotlin源码

fun main() {
    test { 
        print("test方法体")
    }
}

inline fun test(noinline second:() -> Unit){
    second()
}

经过反编译为java字节码后

public final class KotlinDemoKt {
   public static final void main() {
      Function0 second$iv = (Function0)null.INSTANCE;
      int $i$f$test = false;
      second$iv.invoke();
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }

   public static final void test(@NotNull Function0 second) {
      int $i$f$test = 0;
      Intrinsics.checkNotNullParameter(second, "second");
      second.invoke();
   }
}

可以看到虽然你使用了inline关键字,但是在参数中使用了noinline关键字,故参数最终执行时创建了Function0对象,未将数据直接复制到要执行的位置。
注意: noinline只应用于(inline 函数)的参数中。

4.crossinline关键字

在代码中如果要将一个方法退出,你会怎么做?
直接对代码进行return是不错的选择,那么在内联函数中可以使用吗?
示例代码如下:

fun main() {
    println("test执行之前")
    test {
        println("test方法执行")
        return
    }
    println("test执行之后")
}

inline fun test(second:() -> Unit){
    println("second执行之前")
    second()
    println("second执行之后")
}

按照上面描述的使用内联函数的代码会将内联函数方法体复制到所要执行的位置,像这样

fun main() {
    println("test执行之前")
    println("second执行之前")
    println("test方法执行")
    return
    println("test执行之后")
}

示例代码运行结果:

test执行之前
second执行之前
test方法执行

可以看到与我们预期效果相同直接从return处将代码打断了。考虑到可能会有仅仅需要打断代码快中内容kotlin为我们提供了return@test,仅仅只打断Lambda代码块中的内容。 示例代码

fun main() {
    println("test执行之前")
    test {
        println("test方法执行")
        return@test
        println("return@test后面内容")
    }
    println("test执行之后")
}

inline fun test(second:() -> Unit){
    println("second执行之前")
    second()
    println("second执行之后")
}

执行结果:

test执行之前
second执行之前
test方法执行
second执行之后
test执行之后

从结果中可以看到只有return@test后面的println("return@test后面内容")没有打印。所以可以看到只有在代码块

    test {
        println("test方法执行")
        return@test
        println("return@test后面内容")
    }

中的代码会因为return而断掉。在一些特殊场景我们不需要关注其他内容只需要关注Lambda表达式方法体内容时 crossinline关键字就起到了关键性作用。

screenshot-20250124-221808.png 可以看到如果添加crossinline关键字后,不能直接使用return,IDE会爆红。这个关键字就是为了防止用户在Lambda中执行return操作影响整个函数代码执行。 **注意:**crossinline关键字也只能在内联函数中使用。

5.reified关键字

Android开发中对于Activity的跳转代码下面这样写应该是比较常规的流程

fun jump(context: Context, activityClass: Class<Activity>) {
    startActivity(Intent(context, activityClass))
}

非常简单,我就不啰嗦了。看下kotlin为我们提供的语法糖

inline fun <reified T: Activity> jump(context: Context) {
    startActivity(Intent(context, T::class.java))
}

看到没一个参数搞定,但是我们需要传入类型使用方式如下

jump<ViewActivity>(this@MainActivity)

像这样我们只需要通过反省将对象插入进来就可以了。

4.参考

kotlin参考:内联函数 · Kotlin 官方文档 中文版

抛物线inline: inline 和 noinline