Kotlin 之 inline & noline & crossinline

Posted 月盡天明

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Kotlin 之 inline & noline & crossinline相关的知识,希望对你有一定的参考价值。

inline & noline & crossinline


class TestInline 
    @JvmField
    val TAG = "Test"

    fun main() 
        Log.i(TAG, "main")
        test1 
            Log.i(TAG, "test0")
        
    

    private fun test1(test0: () -> Unit) 
        Log.i(TAG, "before test0")
        test0()
        Log.i(TAG, "after test0")

        Log.i(TAG, "before test2")
        test2()
        Log.i(TAG, "after test2")
    

    private fun test2() 
        Log.i(TAG, "test2")
    

根据上面的类作为一个 demo 来进行分析一下:

Decompile TestInline.kt 文件的 Bytecode 得到如下 Java 文件:


public final class TestInline 
   @JvmField
   @NotNull
   public final String TAG = "Test";

   public final void main() 
      Log.i(this.TAG, "main");
      this.test1((Function0)(new Function0() 
         // $FF: synthetic method
         // $FF: bridge method
         public Object invoke() 
            this.invoke();
            return Unit.INSTANCE;
         

         public final void invoke() 
            Log.i(TestInline.this.TAG, "test0");
         
      ));
   

   private final void test1(Function0 test0) 
      Log.i(this.TAG, "before test0");
      test0.invoke();
      Log.i(this.TAG, "after test0");
      Log.i(this.TAG, "before test2");
      this.test2();
      Log.i(this.TAG, "after test2");
   

   private final void test2() 
      Log.i(this.TAG, "test2");
   


会发现

test0: () -> Unit

这个 lambda 表达式被转成了 Function0 这个接口。

Kotlin-stdlib-1.3.11-source.jar

kotlin.jvm.functions.Functions.kt

这个文件内部定义了 23 个 「Function」接口。

test1(test0: () -> Unit)  // 这个函数的调用变成了对匿名内部类的函数回调

相当与lambda函数的调用会产生一些额外开销。

上面的 demo 还只是创建一个匿名内部类来使用,假如是多个呢?

fun main() 
    Log.i(TAG, "main")
    intArrayOf(1, 2, 3, 4, 5).forEach 
        test1 
            Log.i(TAG, "test0 -- $it")
        
    

上面的例子 Decompile 之后如下:

public final void main() 
   Log.i(this.TAG, "main");
   int[] $receiver$iv = new int[]1, 2, 3, 4, 5;
   int[] var2 = $receiver$iv;
   int var3 = $receiver$iv.length;

   for(int var4 = 0; var4 < var3; ++var4) 
      int element$iv = var2[var4];
      int var7 = false;
      this.test1((Function0)(new TestInline$main$$inlined$forEach$lambda$1(element$iv, this)));
   



// 
final class TestInline$main$$inlined$forEach$lambda$1 extends Lambda implements Function0 
   // $FF: synthetic field
   final int $it;
   // $FF: synthetic field
   final TestInline this$0;

   TestInline$main$$inlined$forEach$lambda$1(int var1, TestInline var2) 
      super(0);
      this.$it = var1;
      this.this$0 = var2;
   

   // $FF: synthetic method
   // $FF: bridge method
   public Object invoke() 
      this.invoke();
      return Unit.INSTANCE;
   

   public final void invoke() 
      Log.i(this.this$0.TAG, "test0 -- " + this.$it);
   


假如对 lambda 表达式多次调用,则会产生多次匿名内部类,造成的内存开销就不可被忽视了。


inline

把 test(test0: ()-> Unit) 方法改造一下 ,加上 inline 关键字

fun main() 
    Log.i(TAG, "main")
    test1 
        Log.i(TAG, "test0")
    

// inline
private inline fun test1(test0: () -> Unit) 
    Log.i(TAG, "before test0")
    test0()
    Log.i(TAG, "after test0")

    Log.i(TAG, "before test2")
    test2()
    Log.i(TAG, "after test2")


Decompile 之后:

public final void main() 
   Log.i(this.TAG, "main");
   int $i$f$test1 = false;
   Log.i(this.TAG, "before test0");
   int var3 = false;
   Log.i(this.TAG, "test0");
   Log.i(this.TAG, "after test0");
   Log.i(this.TAG, "before test2");
   access$test2(this);
   Log.i(this.TAG, "after test2");


private final void test1(Function0 test0) 
   int $i$f$test1 = 0;
   Log.i(this.TAG, "before test0");
   test0.invoke();
   Log.i(this.TAG, "after test0");
   Log.i(this.TAG, "before test2");
   access$test2(this);
   Log.i(this.TAG, "after test2");


会发现,在 main 函数中包含了 test1 函数的整个方法体和lambda 表达式的内容。

Inline 的作用就是把被 inline 的方法提取到被调用处。

当出现 lambda 被调用多次的时候,也不会出现生成多个匿名内部类的问题。

fun main() 
    Log.i(TAG, "main")
    intArrayOf(1, 2, 3, 4, 5).forEach 
        test1 
            Log.i(TAG, "test0")
        
    


private inline fun test1(test0: () -> Unit) 
    Log.i(TAG, "before test0")
    test0()
    Log.i(TAG, "after test0")

    Log.i(TAG, "before test2")
    test2()
    Log.i(TAG, "after test2")

Decompile 如下:

public final void main() 
   Log.i(this.TAG, "main");
   int[] $receiver$iv = new int[]1, 2, 3, 4, 5;
   int[] var2 = $receiver$iv;
   int var3 = $receiver$iv.length;

   for(int var4 = 0; var4 < var3; ++var4) 
      int element$iv = var2[var4];
      int var7 = false;
      int $i$f$test1 = false;
      Log.i(this.TAG, "before test0");
      int var10 = false;
      Log.i(this.TAG, "test0");
      Log.i(this.TAG, "after test0");
      Log.i(this.TAG, "before test2");
      access$test2(this);
      Log.i(this.TAG, "after test2");
   



// 省略 test1(Function0 test0)

即是有循环多次调用 test1(),也不会出现多个匿名内部类,而是把 test1() 的方法体整个包括循环调用。


return 处理

fun main() 
    Log.i(TAG, "main")
    test1 
        Log.i(TAG, "test0 -- start")
        return
        Log.i(TAG, "test0 -- end")
    


假如在 lamdba 表达式中加上 return
Decompile 之后:

public final void main() 
   Log.i(this.TAG, "main");
   int $i$f$test1 = false;
   Log.i(this.TAG, "before test0");
   int var3 = false;
   Log.i(this.TAG, "test0 -- start");

return 之后的 Log 语句根本就没有执行,并且 test1() 方法体,test0.invoke()之后的语句也没有执行。

那么怎么只 return lamdba 表达式的部分呢?

使用 return@xxx 「也就是带label 的 return」

fun main() 
    Log.i(TAG, "main")
    test1 
        Log.i(TAG, "test0 -- start")
        return@test1
        Log.i(TAG, "test0 -- end")
    

Decompile之后如下:

public final void main() 
   Log.i(this.TAG, "main");
   int $i$f$test1 = false;
   Log.i(this.TAG, "before test0");
   int var3 = false;
   Log.i(this.TAG, "test0 -- start");
   Log.i(this.TAG, "after test0");
   Log.i(this.TAG, "before test2");
   access$test2(this);
   Log.i(this.TAG, "after test2");

此时会发现,只有 lamdba 表达式中 return 后面的语句没被执行,test1() 方法体的其他语句会正常执行。


noline

noline 是用来限制 lambda 表达式,强制它进行 inline 处理。
noline 必须配合 inline 使用,因为默认方法不进行 inline 处理,lamdba 表达式就是 noinline 的.

还是对上面的demo 进行修改如下:

fun main() 
    Log.i(TAG, "main")
    test1 
        Log.i(TAG, "test0 -- start")
        Log.i(TAG, "test0 -- end")
    


private inline fun test1(noinline test0: () -> Unit) 
    Log.i(TAG, "before test0")
    test0()
    Log.i(TAG, "after test0")

    Log.i(TAG, "before test2")
    test2()
    Log.i(TAG, "after test2")

Decompile 如下:

public final void main() 
   Log.i(this.TAG, "main");
   Function0 test0$iv = (Function0)(new Function0() 
      // $FF: synthetic method
      // $FF: bridge method
      public Object invoke() 
         this.invoke();
         return Unit.INSTANCE;
      

      public final void invoke() 
         Log.i(TestInline.this.TAG, "test0 -- start");
         Log.i(TestInline.this.TAG, "test0 -- end");
      
   );
   int $i$f$test1 = false;
   Log.i(this.TAG, "before test0");
   test0$iv.invoke();
   Log.i(this.TAG, "after test0");
   Log.i(this.TAG, "before test2");
   access$test2(this);
   Log.i(this.TAG, "after test2");

会发现,除了 lamdba 表达式,test1()的方法体都被内敛。而 lamdba 表达式还是用内部类的方式进行实现。

另外,noinline 修饰的 lamdba 表达式不能使用 return 关键字。

但是可以使用 return@xxx


crossinline

加入想使用 inline 的特性,又不想在 lamdba 表达式中直接有 return 的情况出现,则可以使用 crossinline

private inline fun test1(crossinline test0: () -> Unit) 
	// ...

编译器直接会提示不允许在此处使用 return

Decompile 之后如下:

public final void main() 
   Log.i(this.TAG, "main");
   int $i$f$test1 = false;
   Log.i(this.TAG, "before test0");
   int var3 = false;
   Log.i(this.TAG, "test0 -- start");
   Log.i(this.TAG, "test0 -- end");
   Log.i(this.TAG, "after test0");
   Log.i(this.TAG, "before test2");
   access$test2(this);
   Log.i(this.TAG, "after test2");

完全不影响 inline 的效果。

But, return@xxx 是可以正常使用的。


break & continue

官网上有这么一句话:

breakandcontinueare not yet available in inlined lambdas, but we are planning to support them too.

break和continue在内联的 lambda 表达式中还不可用,但也计划支持它们。

记得很早前写了一篇文章关于 forEach 跳出循环的Kotlin 之 forEach 跳出循环 - crazy_jack - CSDN博客

现在来看下 forEach 方法的定义:

public inline fun IntArray.forEach(action: (Int) -> Unit): Unit 
    for (element in this) action(element)

发现它是一个 inline 方法。这就会导致使用 return@xxx 的方式不能退出整个循环体。

所以想要跳出forEach 的循环体,继续往下执行,请参考这篇 blog 中的写法。

总结

inline 的好处:

  • 减少方法调用产生的进栈和出栈操作,提升运行时的效率
  • kotlin 的 inline 发生在编译时期

reference

以上是关于Kotlin 之 inline & noline & crossinline的主要内容,如果未能解决你的问题,请参考以下文章

kotlin中的匿名函数&Lambda

Kotlin:你必须要知道的 inline-noinline-crossinline

Kotlin inline 内联函数

Kotlin Inline 的原理和注意点

Kotlin Inline 的原理和注意点

Kotlin 1.5 新特性 Inline classes,还不了解一下?