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 表达式多次调用,则会产生多次匿名内部类,造成的内存开销就不可被忽视了。
把 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() 的方法体整个包括循环调用。
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 是用来限制 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
加入想使用 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 是可以正常使用的。
官网上有这么一句话:
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 的好处: