在Kotlin 中使用 Lambda表达式会带来一些额外的开销。但可以通过内联函数优化。
一. 优化Lambda开销
在Kotlin中每次声明一个Lambda表达式,就会在字节码中产生一个匿名类。该匿名类包含了一个invoke方法,作为Lambda的调用方法,每次调用的时候,还会创建一个新的对象。可想而知,Lambda虽然简洁,但是会增加额外的开销。Kotlin 采用内联函数来优化Lambda带来的额外开销。
1.1 invokedynamic
Java如何解决优化Lambda的问题的呢?与Kotlin这种在编译期通过硬编码生成Lambda转换类的机制不同,Java在SE7之后,通过invokedynamic技术实现了在运行期间才产生相应的翻译代码。在invokedynamic被首次调用的时候,就会触发产生一个匿名内部类来替换中间码invokedynamic,后续的调用会直接采用这个匿名类的代码。
这样做的好处是:
1.由于具体的转换实现是在运行时产生的,在字节码中能看到的只有一个固定的invokedynamic,所以需要静态生成的类的个数以及字节码大小显著减少。
2.与编译时写死在字节码中的策略不同,利用invokedynamic可以把实际的翻译策略隐藏在JDK库的实现,提高了灵活性,在确保向后兼容性的同时,后期可以继续对翻译策略不断优化升级。
3.JVM天然支持类针对该方式对Lambda表达式的翻译和优化,开发者不必考虑这个问题。
1.2内联函数
invokedynamic虽然不错,但是Kotlin需要兼容Android最主流的Java版本SE6,这导致Kotlin无法使用invokedynamic来解决android平台Lambda开销的问题。所以Kotlin使用内联函数来解决这个问题,在Kotlin中使用inline关键字来修饰函数,这些函数就成了内联函数。它们的函数体在编译的时期被嵌入到每一个调用的地方,以减少额外生成的匿名类数,以及函数执行的时间开销。
2.内联函数具体语法
声明一个高阶函数payFoo,可以接收一个类型为()->Unit的Lambda,然后在main函数中调用它。
fun main() {
payFoo {
println("write kotlin...")
}
}
fun payFoo(block: () -> Unit) {
println("before block")
block()
println("end block")
}
通过字节码反编译的相关Java代码。
public final class InlineDemoKt {
public static final void main() {
payFoo((Function0)null.INSTANCE);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
public static final void payFoo(@NotNull Function0 block) {
Intrinsics.checkParameterIsNotNull(block, "block");
String var1 = "before block";
System.out.println(var1);
block.invoke();
var1 = "end block";
System.out.println(var1);
}
}
可以发现调用payFoo后就会产生一个Function0类型的block类,然后通过invoke 方法来执行,这会增加额外的生成类和函数调用开销。
现在通过inline修饰payFoo函数:
fun main() {
payFoo {
println("write kotlin...")
}
}
inline fun payFoo(block: () -> Unit) {
println("before block")
block()
println("end block")
}
public final class InlineDemoKt {
public static final void main() {
int $i$f$payFoo = false;
String var1 = "before block";
System.out.println(var1);
int var2 = false;
String var3 = "write kotlin...";
System.out.println(var3);
var1 = "end block";
System.out.println(var1);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
public static final void payFoo(@NotNull Function0 block) {
int $i$f$payFoo = 0;
Intrinsics.checkParameterIsNotNull(block, "block");
String var2 = "before block";
System.out.println(var2);
block.invoke();
var2 = "end block";
System.out.println(var2);
}
}
可以发现通过inline修饰的函数,其函数体代码被调用的Lambda代码都粘贴到了相应调用的位置。试想一下,如果这是一个工程中公共的方法,或者被嵌套在一个循环调用的逻辑体中,这个方法必然会被调用多次。通过inline语法,可以彻底消除这种额外调用,从而节约了开销。
内联函数典型的一个应用场景就是Kotlin的集合类。如果你看过 Kotlin的集合类API文档或者源码实现,可以发现,集合函数式API,如map、filter都被定义成了内联函数。
/**
* Returns a list containing the results of applying the given [transform] function
* to each element in the original array.
*/
public inline fun Array.map(transform: (T) -> R): List {
return mapTo(ArrayList(size), transform)
}
/**
* Returns a list containing only elements matching the given [predicate].
*/
public inline fun Array.filter(predicate: (T) -> Boolean): List {
return filterTo(ArrayList(), predicate)
}
因为上面的方法都接收Lambda作为参数,同时需要对集合元素进行遍历操作,所以会定义为内联函数。
内联函数不是万能的,以下情况避免使用内联函数:
1.由于JVM对普通函数已经能够根据实际情况智能地判断是否进行内联优化,所以我们并不需要对其使用Kotlin的inline语法,那只会让字节码变得更加复杂。
2.尽量避免对具有大量函数体的函数进行内联,这样会导致过多的字节码数量。
3.一旦一个函数被定义为内联函数,便不能获取闭包类的私有成员,除非你把它们声明为internal。
下面就是错误写法,本质是private修饰的a没有get 方法。
class TestPay {
private var a = 1 //错误写法,会报错
inline fun printNumber() {
println(a)
}
}
二.noinline :避免参数被内联
如果一个函数的开头加上inline修饰符,那么它的函数体以及Lambda参数都会被内联。然而现实中的情况比较复杂,有一种可能是函数需要接收多个参数,但我们只想对其中部分Lambda参数内联,其他的则不内联,应该如何处理?
Kotlin引入了noinline关键字,可以加在不想要内联的参数开头,该参数便不会有内联效果。
fun main() {
payFoo({
println("I am inlined...")
}, {
println("I am not inlined...")
})
}
inline fun payFoo(block1: () -> Unit, noinline block2: () -> Unit) {
println("before block")
block1()
block2()
println("end block")
}
before block
I am inlined...
I am not inlined...
end block
可以发现block1被内联了,block2没有被内联。
public final class NoinlineDemoKt {
public static final void main() {
Function0 block2$iv = (Function0)null.INSTANCE;
int $i$f$payFoo = false;
String var2 = "before block";
System.out.println(var2);
int var3 = false;
//可以发现block1被内联了。
String var4 = "I am inlined...";
System.out.println(var4);
//block2没有被内联
block2$iv.invoke();
var2 = "end block";
System.out.println(var2);
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
public static final void payFoo(@NotNull Function0 block1, @NotNull Function0 block2) {
int $i$f$payFoo = 0;
Intrinsics.checkParameterIsNotNull(block1, "block1");
Intrinsics.checkParameterIsNotNull(block2, "block2");
String var3 = "before block";
System.out.println(var3);
block1.invoke();
block2.invoke();
var3 = "end block";
System.out.println(var3);
}
}
从上面的代码中可以看出,payFoo函数中block2函数在带上noinline之后,反编译后的Java代码中没有将其函数体代码在调用处进行替换。
三.非局部返回
Kotlin中内联函数除了优化Lambda开销之外,还带来了非局部返回和具体化参数类型。
1.Kotlin如何支持非局部返回
首先看常见的局部返回的例子
fun main() {
payFoo()
}
fun localReturn() {
return
}
fun payFoo() {
println("before local return")
localReturn()
println("after local return")
return
}
before local return
after local return
从上面代码可以发现,localReturn执行之后,其函数体中的return只会在该函数的局部生效,所以localReturn()之后的println函数依旧生效。
如何把函数换成Lambda版本,(存在问题的写法)
fun main() {
payFoo { return }
}
fun payFoo(returning: () -> Unit) {
println("before local return")
returning()
println("after local return")
return
}
'return' is not allowed here
从上面的代码可以发现报错了,在Kotlin中,正常情况下,Lambda表达式不允许存在return关键字。
通过内联函数修改
fun main() {
payFoo { return }
}
inline fun payFoo(returning: () -> Unit) {
println("before local return")
returning()
println("after local return")
return
}
before local return
编译通过,但结果与局部返回不同,Lambda的return执行之后直接让foo函数退出了执行。因为内联函数payFoo的函数体及参数Lambda会直接替代具体的调用,所以实际产生的代码中,return相当于是直接暴露在main函数中的,所以returning之后的代码就不会执行了,这就是非局部返回。
public final class Testinline3Kt {
public static final void main() {
int $i$f$payFoo = false;
String var1 = "before local return";
System.out.println(var1);
int var2 = false;
}
// $FF: synthetic method
public static void main(String[] var0) {
main();
}
public static final void payFoo(@NotNull Function0 returning) {
int $i$f$payFoo = 0;
Intrinsics.checkParameterIsNotNull(returning, "returning");
String var2 = "before local return";
System.out.println(var2);
returning.invoke();
var2 = "after local return";
System.out.println(var2);
}
}
使用标签实现Lambda非局部返回
另一种等效的方式,是通过标签利用@符号来实现Lambda非局部返回。可以在不声明inline修饰符的情况下,实现相同效果。
fun main() {
payFoo2 { return@payFoo2 }
}
fun payFoo2(returning: () -> Unit) {
println("before local return")
returning()
println("after local return")
return
}
before local return
after local return
非局部返回尤其在循环控制中特别有用,比如Kotlin的forEach接口,它接收一个Lambda参数,由于它也是一个内联函数,所以可以直接在它调用的Lambda中执行return退出上一层的程序。
fun main() {
println(hasZero(listOf(0,2,3)))
}
fun hasZero(list: List): Boolean {
list.forEach {
if (it == 0) return true //直接返回结果
}
return false
}
true
crossinline
非局部返回在某些场合下非常有用,但可能存在风险,内联的函数所接收的Lambda参数常常来自于上下文的其他地方。为了避免带有return的Lambda参数产生破坏,可以使用crossinline 关键字来修饰该参数。
fun main() {
payFoo3 {
return
}
}
inline fun payFoo3(crossinline returning: () -> Unit) {
println("before local return")
returning()
println("after local return")
return
}
'return' is not allowed here
四.具体化参数类型
内联函数可以帮助我们实现具体化参数类型,Kotlin与Java一样,由于运行时的类型擦除,我们不能直接获取一个参数的类型。然而,由于内联函数会直接在字节码中生成相应的函数体实现,这时候反而可以获得参数的具体类型。
使用reified修饰符来实现这一效果。
fun main() {
getType()
}
inline fun getType() {
println(T::class)
}
class java.lang.Integer (Kotlin reflection is not available)
这个特性在android开发中也特别有用,在Java中,需要调用startActivity时,通常需要把具体的目标视图类作为一个参数。但是在Kotlin中,可以使用reified来进行简化。
import android.app.Activity
import android.content.Intent
inline fun Activity.startActivity() {
startActivity(Intent(this, T::class.java))
}
参考<