kotlin inline

使用高阶函数会带来一些运行时的效率损失:每一个函数都是一个对象,并且会捕获一个闭包。即那些在函数体内会访问到的变量。内存分配和虚拟调用会引入运行时间开销。

前言

Kotlin在集合API中大量使用了Lambda,这使得我们在对集合进行操作的时候优雅了许多。但是这种方式的代价就是,在Kotlin中使用Lambda表达式会带来一些额外的开销,而内联函数应运而生,用来解决优化Kotlin支持Lambda表达式之后所带来的开销。

优化Lambda开销

Kotlin默认面向JDK6,而JDK8才引入Lambda表达式支持。在Kotlin中每申明一个Lambda表达式,就会在字节码中产生一个匿名类,该匿名类包含了一个invoke方法,作为Lambda的调用方法,每次调用都会创建一个新的对象,必须采用某种方法来优化Lambda带来的额外开销,也就是内联函数。

invokedynamic

与Kotlin这种在编译期通过硬编码生成Lambda转换类的机制不同,Java在SE 7之后通过invokedynamic技术实现了在运行期才产生相应的翻译代码。在invokedynamic被首次调用的时候,就会触发产生一个匿名类来替换中间码invokedynamic,后续的调用会直接采用这个匿名类的代码,这样做的好处主要体现在:

  • 由于具体的转换实现是在运行时产生的,在字节码中能看到的只有一个固定的invokedynamic,所以需要静态生成的类的个数及字节码大小都显著减少。
  • 编译时写死在字节码中的策略不同,利用invokedynamic可以把实际的翻译策略隐藏在JDK库的实现,这极大提高了灵活性,在确保向后兼容性的同时,后期可以继续对翻译策略不断优化升级。
  • JVM天然支持了针对该方式的Lambda表达式的翻译和优化,这也意味着开发者在书写Lambda表达式的同时,可以完全不用关心这个问题,这极大的提高了开发体验(体验很重要)。

内联函数

invokedynamic固然不错,但Kotlin不支持它的理由也很充分。我们有足够的理由相信,其最大的原因是Kotlin在一开始就需要兼容Android最主流的Java版本SE 6,这导致无法通过invokedynamic来解决Android平台的Lambda开销问题。因此作为另外一种主流的解决方案,Kotlin拥抱了内联函数,在C++、C#等语言中也支持这种特性。简单来说,我们可以用inline关键字来修饰函数,这些函数就成了内联函数。它们的函数体在编译期被嵌入每一个被调用的地方,以减少额外生成的匿名类数,以及函数执行的时间开销。

inline语法

我们看一下内联函数如何操作

fun main(args:Array<String>) {

foo {
println("dive into kotlin...")
}
}

fun foo(block: () -> Unit) {
block()
}

申明一个高阶函数foo,接收一个类型为()->Unit的Lambda,并在main函数中调用它。以下是通过字节码反编译得到的Java代码:

public static final void foo(@NotNull Function0 block) {

Intrinsics.checkParameterIsNotNull(block, "block");
block.invoke();
}

public static final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
foo((Function0)null.INSTANCE);
}

调用foo就会产生一个Function0类型的block类,然后通过invoke方法来执行,这会增加额外的生成类和调用开销,现在我们给foo函数加上inline修饰符,如下:

inline fun foo(block:() -> Unit) {

block()
}
 public static final void main(@NotNull String[] args) {

Intrinsics.checkParameterIsNotNull(args, "args");
int $i$f$foo = false;
int var2 = false;
String var3 = "xxxx";
boolean var4 = false;
System.out.print(var3);
}

果然,foo函数体代码及被调用的Lambda代码都粘贴到了相应调用的位置,如果这是一个工程中的公共的方法,或者被嵌套在一个循环调用的逻辑体中,通过inline语法糖,我们可以彻底消除这种额外调用,从而节约开销。

一些情况下应该避免使用inline

  • JVM对普通的函数已经能够根据实际情况智能地判断是否进行内联优化,所以我们并不需要对其使用Kotlin的inline语法,那只会让字节码变得更加复杂
  • 尽量避免对具有大量函数体的函数进行内联,这样会导致过多的字节码数量
  • 一旦一个函数被定义为内联函数,便不能获取闭包类的私有成员,除非被声明为internal

避免参数被内联noinline

现实中情况往往十分复杂,可能多个参数时,我们只想对部分Lambda参数内联,其他不内联,这个时候就需要noinline关键字

inline fun foo (block1 () -> Unit,noinline block2:()->Unit) {

block1()
block2()
}

非局部返回

Kotlin中的内联函数除了优化Lambda开销之外,还带来了其他方面的特效,典型的就是非局部返回和具体化参数类型。

使用

    fun main(args: Array<String>) {

foo()
}

fun localReturn() {
return
}

fun foo() {
println("before local return")
localReturn()
println("after local return")
return
}
//运行结果
//before local return
//after local return

localReturn执行后,其函数体中的return只会在该函数的局部生效,所以localReturn()之后的println函数依旧生效。换成Lambda表达式的版本:

    fun main(args: Array<String>) {

foo {return}
}

fun foo (returning: () -> Unit) {
println("before local return")
returning()
println("after local return")
return
}

//运行结果
// Error:(2,11) Kotlin: 'return' is not allowed here

编译报错,正常情况下Lambda表达式不允许存在return关键字,这时候内联函数就派上用场了,foo函数进行内联后:

//运行结果

//before local return

内联函数foo的函数体及参数Lambda会直接替代具体的调用,所以实际产生的代码中,return相当于直接暴露在main函数中,所以returning()之后的代码自然不会被执行。这个就是所谓的非局部返回

使用标签实现Lambda非局部返回

另外一种等效的方式,是通过标签利用@符号来实现Lambda非局部返回,我们可以在不申明inline的情况下,实现同样的效果:

    fun main(args: Array<String>) {

foo (return@foo)
}

fun foo(returning: () -> Unit) {
println("before local return")
returning()
println("after local return")
return
}
//运行结果
//before local return

crossinline

我们内联函数所接收的Lambda参数常常来自于上下文其他地方,为了避免带有return的Lambda参数产生破坏,我们可以是crossinline来修饰该参数,从而杜绝该问题的发生。

fun main(args:Array<String>) {
foo {return}//return会有下滑波浪线,IDEA会报错
}
inline fun foo(crossinline returning:()->Unit) {
println("before")
returning()
println("after")
return
}

具体化参数类型

除了非局部返回之外,内联函数还可以帮助Kotlin实现具体化参数类型,Kotlin与Java一样,由于运行时的类型擦拭,我们并不能直接获取一个参数的类型。然而,由于内联函数会直接在字节码中生成相应的函数体实现,这种情况下我们反而可以获得参数的具体类型,可以用reified修饰符来实现这一效果

    fun main(args:Array<String>){

getType<Int>()
}

inline fun <reified T>getType() {
print(T::class)
}
//运行结果
//class Kotlin.Int

这一特性在Android中格外有用。比如,当我们要调用startActivity时,通常需要把具体的目标视图类作为参数传递,而在Kotlin中:

inline fun <reified T : Activity> Activity.startActivity() {

startActivity(Intent(this,T::class.java))
}

//usage
startActivity<LoginActivity>()

END

作者

8MilesRD

发布于

2019-11-18

更新于

2019-11-19

许可协议

评论