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语法
我们看一下内联函数如何操作
|
申明一个高阶函数foo,接收一个类型为()->Unit的Lambda,并在main函数中调用它。以下是通过字节码反编译得到的Java代码:
|
调用foo就会产生一个Function0类型的block类,然后通过invoke方法来执行,这会增加额外的生成类和调用开销,现在我们给foo函数加上inline修饰符,如下:
|
|
果然,foo函数体代码及被调用的Lambda代码都粘贴到了相应调用的位置,如果这是一个工程中的公共的方法,或者被嵌套在一个循环调用的逻辑体中,通过inline语法糖,我们可以彻底消除这种额外调用,从而节约开销。
一些情况下应该避免使用inline
- JVM对普通的函数已经能够根据实际情况智能地判断是否进行内联优化,所以我们并不需要对其使用Kotlin的inline语法,那只会让字节码变得更加复杂
- 尽量避免对具有大量函数体的函数进行内联,这样会导致过多的字节码数量
- 一旦一个函数被定义为内联函数,便不能获取闭包类的私有成员,除非被声明为internal
避免参数被内联noinline
现实中情况往往十分复杂,可能多个参数时,我们只想对部分Lambda参数内联,其他不内联,这个时候就需要noinline关键字
|
非局部返回
Kotlin中的内联函数除了优化Lambda开销之外,还带来了其他方面的特效,典型的就是非局部返回和具体化参数类型。
使用
|
localReturn执行后,其函数体中的return只会在该函数的局部生效,所以localReturn()之后的println函数依旧生效。换成Lambda表达式的版本:
|
编译报错,正常情况下Lambda表达式不允许存在return关键字,这时候内联函数就派上用场了,foo函数进行内联后:
|
内联函数foo的函数体及参数Lambda会直接替代具体的调用,所以实际产生的代码中,return相当于直接暴露在main函数中,所以returning()之后的代码自然不会被执行。这个就是所谓的非局部返回。
使用标签实现Lambda非局部返回
另外一种等效的方式,是通过标签利用@符号来实现Lambda非局部返回,我们可以在不申明inline的情况下,实现同样的效果:
|
crossinline
我们内联函数所接收的Lambda参数常常来自于上下文其他地方,为了避免带有return的Lambda参数产生破坏,我们可以是crossinline来修饰该参数,从而杜绝该问题的发生。
|
具体化参数类型
除了非局部返回之外,内联函数还可以帮助Kotlin实现具体化参数类型,Kotlin与Java一样,由于运行时的类型擦拭,我们并不能直接获取一个参数的类型。然而,由于内联函数会直接在字节码中生成相应的函数体实现,这种情况下我们反而可以获得参数的具体类型,可以用reified修饰符来实现这一效果
|
这一特性在Android中格外有用。比如,当我们要调用startActivity时,通常需要把具体的目标视图类作为参数传递,而在Kotlin中:
|
END