本文永久更新地址: https://xiaozhuanlan.com/topic/3458207169java
重学 Kotlin 已经来到了第三期,前面已经介绍了:web
今天的主角是 inline ,这不是一个 Kotlin 特有的概念,大多数编程语言都支持内联。编程语言
内联函数 的语义很简单: 把函数体复制粘贴到函数调用处 。使用起来也毫无困难,用 inline
关键字修饰函数便可。编辑器
然而问题的关键并非如何使用 inline
,而是何时使用 inline
? 既然 Kotlin 提供了内联,它确定是为了性能优化而存在的,那么,它又真的是包治百病的性能良药吗?ide
今天,咱们就一块儿来刨根挖底,寻找一下答案。函数
前面已经说过 inline
就是 把函数体复制粘贴到函数调用处 ,彻底是编译器的小把戏。本着严谨科学的态度,咱们仍是来反编译验证一下。性能
优化
inline fun test() { println("I'm a inline function") } 复制代码fun run() { test() } 复制代码
在 run()
函数中调用了内联函数 test()
。反编译查看对应的 java 代码:
public static final void test() {
String var1 = "I'm a inline function"; System.out.println(var1); } public static final void run() { String var1 = "I'm a inline function"; System.out.println(var1); } 复制代码
能够看到 run()
函数中并无直接调用 test()
函数,而是把 test()
函数的代码直接放入本身的函数体中。这就是 inline
的功效。
那么,问题就来了。这样就能够提升运行效率吗?若是能够,为何?
咱们先从 JVM 的方法执行机制提及。
JVM 进行方法调用和方法执行依赖 栈帧,每个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。
线程的栈帧是存储在虚拟机栈中,以上面示例代码的 未内联 版本为例,对应的方法执行过程和对应的栈帧结构以下所示:
未内联的状况下,整个执行过程当中会产生两个方法栈帧,每个方法栈帧都包括了 局部变量表、操做数栈、动态链接、方法返回地址和一些额外的附加信息 。
使用内联的状况下,只须要一个方法栈帧,下降了方法调用的成本。
乍一看,的确的提升了运行效率,毕竟少用一个栈帧嘛。
然而?
一切看起来都很美好,除了 IDE 给个人刺眼提示。
Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types
大体意思是在这里使用内联对性能的影响微乎其微,或者说没有什么意义。Kotlin 的内联最好用在函数参数类型中。
不急着解释,首先来一发灵魂拷问。
你能够说不支持,由于 Java 并无提供相似 inline
的显示声明内联函数的方法。
可是 JVM 是支持的。Java 把内联优化交给虚拟机来进行,从而避免开发者的滥用。
典型的一种滥用, 内联超长方法 ,极大的增大字节码长度,反而得不偿失。你能够注意 Kotlin 标准库中的内联函数,基本都是简短的函数。
对于普通的函数调用,JVM 已经提供了足够的内联支持。所以,在 Kotlin 中,没有必要为普通函数使用内联,交给 JVM 就好了。
另外,Java 代码是由 javac
编译的,Kotlin 代码是由 kotlinc
编译的,而 JVM 能够对字节码作统一的内联优化。因此,能够推断出,不论是 javac ,仍是 kotlinc,在编译期是没有内联优化的。
至于 JVM 具体的内联优化机制,我了解的并很少,这里就不作过多介绍了。后续若是我看到相关资料,会到这里进行补充。
因此,上一节中 IDE 给开发者的提示就很明了了。
JVM 已经提供了内联支持,因此没有必要在 Kotlin 中内联普通函数。
那么问题又来了。 既然 JVM 已经支持内联优化,Kotlin 的内联存在的意义是什么 ? 答案就是 Lambda
。
为何要拯救 Lambda,咱们首先得知道Kotlin 的 Lambda 对于 JVM 而言到底是什么。
Kotlin 标准库中有一个叫 runCatching
的函数,我在这里实现一个简化版本 runCatch
,参数是一个函数类型。
fun runCatch(block: () -> Unit){
try { block() }catch (e:Exception){ e.printStackTrace() } } fun run(){ runCatch { println("xxx") } } 复制代码
反编译生成的 Java 代码以下所示:
public final class InlineKt {
public static final void runCatch(@NotNull Function0<Unit> block) { Intrinsics.checkParameterIsNotNull(block, (String)"block"); try { block.invoke(); } catch (Exception e) { e.printStackTrace(); } } public static final void run() { InlineKt.runCatch((Function0<Unit>)((Function0)run.1.INSTANCE)); } } static final class InlineKt.run.1 extends Lambda implements Function0<Unit> { public static final InlineKt.run.1 INSTANCE = new /* invalid duplicate definition of identical inner class */; public final void invoke() { String string = "xxx"; boolean bl = false; System.out.println((Object)string); } InlineKt.run.1() { } } 复制代码
Kotlin 自诞生之初,就以 兼容 Java 为首要目标。所以,Kotlin 对于 Lambda 表达式的处理是编译生成匿名类。
通过编译器编译以后, runCatch()
方法中的 Lambda 参数被替换为 Function0<Unit>
类型,在 run()
方法中实际调用 runCatch()
时传入的参数是实现了 Function0<Unit>
接口的 InlineKt.run.1
,并重写 了 invoke()
方法。
因此,调用 runCatch()
的时候,会建立一个额外的类 InlineKt.run.1
。这是 Lambda 没有捕捉变量的场景。若是捕捉了变量,表现会怎么样?
fun runCatch(block: () -> Unit){
try { block() }catch (e:Exception){ e.printStackTrace() } } fun run(){ var message = "xxx" runCatch { println(message) } } 复制代码
在 Lambda 内部捕捉了外部变量 message
,其对应的 java 代码以下所示:
public final class InlineKt {
public static final void runCatch(@NotNull Function0<Unit> block) { Intrinsics.checkParameterIsNotNull(block, (String)"block"); try { block.invoke(); } catch (Exception e) { e.printStackTrace(); } } public static final void run() { void message; Ref.ObjectRef objectRef = new Ref.ObjectRef(); objectRef.element = "xxx"; // 这里每次运行都会 new 一个对象 InlineKt.runCatch((Function0<Unit>)((Function0)new Function0<Unit>((Ref.ObjectRef)message){ final /* synthetic */ Ref.ObjectRef $message; public final void invoke() { String string = (String)this.$message.element; boolean bl = false; System.out.println((Object)string); } { this.$message = objectRef; super(0); } })); } } 复制代码
若是 Lambda 捕捉了外部变量,那么每次运行都会 new 一个持有外部变量值的 Function0<Unit>
对象。这比未发生捕捉变量的状况更加糟糕。
总而言之,Kotlin 的 Lambda 为了彻底兼容到 Java6,不只增大了编译代码的体积,也带来了额外的运行时开销。为了解决这个问题,Kotlin 提供了 inline
关键字。
Kotlin 内联函数的做用是消除 lambda 带来的额外开销
给 runCatch()
加持 inline :
inline fun runCatch(block: () -> Unit){
try { block() }catch (e:Exception){ e.printStackTrace() } } fun run(){ var message = "xxx" runCatch { println(message) } } 复制代码
反编译查看 java 代码:
public static final void run() {
Object message = "xxx"; boolean var1 = false; try { int var2 = false; System.out.println(message); } catch (Exception var5) { var5.printStackTrace(); } } 复制代码
runCatch()
的代码被直接内联到 run()
方法中,没有额外生成其余类,消除了 Lambda 带来的额外开销。
既然 Kotlin 的 Lambda 存在性能问题,那旁边的 Java 大兄弟确定也逃脱不了。
从 Java8 开始,Java 借助 invokedynamic
来完成的 Lambda 的优化。
invokedynamic
用于支持动态语言调用。在首次调用时,它会生成一个调用点,并绑定该调用点对应的方法句柄。后续调用时,直接运行该调用点对应的方法句柄便可。说直白一点,第一次调用 invokeddynamic
时,会找到此处应该运行的方法并绑定, 后续运行时就直接告诉你这里应该执行哪一个方法。
关于 invokedynamic
的详细介绍,能够阅读极客时间专栏 《深刻拆解Java虚拟机》的第 8,9 两讲。
一个高阶函数一旦被标记为内联,它的方法体和全部 Lambda 参数都会被内联。
inline fun test(block1: () -> Unit, block2: () -> Unit) {
block1() println("xxx") block2() } 复制代码
test()
函数被标记为了 inline
,因此它的函数体以及两个 Lambda 参数都会被内联。可是因为我要传入的 block1
代码块巨长(或者其余缘由),我并不想将其内联,这时候就要使用 noinline
。
inline fun test(noinline block1: () -> Unit, block2: () -> Unit) {
block1() println("xxx") block2() } 复制代码
这样, block1
就不会被内联了。篇幅缘由,这里就不展现 Java 代码了,相信你也能很容易理解 noinline
。
首先,普通的 lambda 是不容许直接使用 return
的 。
fun runCatch(block: () -> Unit) {
try { print("before lambda") block() print("after lambda") } catch (e: Exception) { e.printStackTrace() } } fun run() { // 普通 lambda 不容许 return runCatch { return } } 复制代码
上面的代码没有办法经过编译,IDE 会提示你 return is not allowed here 。 而 inline
可让咱们突破这个限制。
// 标记为 inline
inline fun runCatch(block: () -> Unit) { try { print("before lambda") block() print("after lambda") } catch (e: Exception) { e.printStackTrace() } } fun run() { runCatch { return } } 复制代码
上面的代码是能够正常编译运行的。和以前的例子惟一的区别就是多了 inline
。
既然容许 return
,那么这里到底是从 Lambda 中返回,继续运行后面的代码?仍是直接结束外层函数的运行呢?看一下 run()
方法的执行结果。
before lambda
复制代码
从运行结果来看,是直接结束外层函数的运行。其实不难理解,这个 return 是直接内联到 run()
方法内部的,至关于在 run()
方法中直接调用 return
。从反编译的 java 代码看,一目了然。
public static final void run() {
boolean var0 = false; try { String var1 = "before lambda"; System.out.print(var1); int var2 = false; } catch (Exception var3) { var3.printStackTrace(); } } 复制代码
编译器直接把 return
以后的代码优化掉了。这样的场景叫作 non-local return (非局部返回) 。
可是有些时候我并不想直接退出外层函数,而是仅仅退出 Lambda 的运行,就能够这样写。
inline fun runCatch(block: () -> Unit) {
try { print("before lambda") block() print("after lambda") } catch (e: Exception) { e.printStackTrace() } } fun run() { // 从 lambda 中返回 runCatch { return@runCatch } } 复制代码
return@label
,这样就会继续执行 Lambda 以后的代码了。这样的场景叫作 局部返回 。
还有一种场景,我是 API 的设计者,我不想 API 使用者进行非局部返回 ,改变个人代码流程。同时我又想使用 inline ,这样实际上是冲突的。前面介绍过,内联会让 Lambda 容许非局部返回。
crossinline
就是为了解决这一冲突而生。它能够在保持内联的状况下,禁止 lambda 从外层函数直接返回。
inline fun runCatch(crossinline block: () -> Unit) {
try { print("before lambda") block() print("after lambda") } catch (e: Exception) { e.printStackTrace() } } fun run() { runCatch { return } } 复制代码
添加 crossinline
以后,上面的代码将没法编译。但下面的代码仍然是能够编译运行的。
inline fun runCatch(crossinline block: () -> Unit) {
try { print("before lambda") block() print("after lambda") } catch (e: Exception) { e.printStackTrace() } } fun run() { runCatch { return@runCatch } } 复制代码
crossinline
能够阻止非局部返回,但并不能阻止局部返回,其实也没有必要。
关于内联函数,一口气说了这么多,总结一下。
在 Kotlin 中,内联函数是用来弥补高阶函数中 Lambda 带来的额外运行开销的。对于普通函数,没有必要使用内联,由于 JVM 已经提供了必定的内联支持。
对指定的 Lambda 参数使用 noinline
,能够避免该 Lambda 被内联。
普通的 Lambda 不支持非局部返回,内联以后容许非局部返回。既要内联,又要禁止非局部返回,请使用 crossinline
。
除了内联函数以外,Kotlin 1.3 开始支持 inline class ,但这是一个实验性 API,须要手动开启编译器支持。不知道你们对内联类有什么独特的见解,欢迎在评论区交流。
本文使用 mdnice 排版
这里是秉心说,关注我,不迷路!