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() 方法体的其他语句会正常执行。

Kotlin 之 inline & noline & crossinline_第1张图片


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 关键字。

Kotlin 之 inline & noline & crossinline_第2张图片

但是可以使用 return@xxx

Kotlin 之 inline & noline & crossinline_第3张图片


crossinline

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

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

Kotlin 之 inline & noline & crossinline_第4张图片

编译器直接会提示不允许在此处使用 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

  • Inline Functions and Reified Type Parameters - Kotlin Programming Language
  • Kotlin 中的 Lambda 与 Inline - 云+社区 - 腾讯云

你可能感兴趣的:(kotlin从零单排,Android从零单排,Android高分局,kotlin,inline,noinline,crossinline,return@)