Kotlin 的内联
变量内联
Java 中有个概念叫做编译时常量—Compile-time Constant,直观的说就是这个变量是不变的,编译器在编译期间就可以确定这个变量的值。具体到代码上,就是这个变量需要是 final
,并且只能是基本类型或者字符串。
这种编译时常量,会被编译器以内联的形式进行编译,也就是直接那你的值去替换调用处的变量名来编译。这样一来,程序结构就会变得简单,编译器和 JVM 也方便做各种优化。
这种编译时常量,到了 Kotlin 中有一个转有的关键字叫 const
:一个变量如果以 const val
开头,它就会被编译器当做编译时常量进行内联式编程。
1 | const val name = "jack" |
上面的代码实际编译是下面这样子的:
1 | fun main() { |
函数内联
inline
让变量内联的是 const
,除了变量,Kotlin 还添加了对函数内联的支持。在 Kotlin 中,你可以给一个函数添加 inline
关键字,这个函数就会以内联的方式进行编译。
1 | inline fun hello() { |
上面的代码实际编译的样子如下:
1 | fun main() { |
函数内联就像上面说的会去掉一个函数,调用栈少了一层,性能损耗肯定会少一些。但实际上调用栈本身所造成的性能损耗非常小,这个优化和没优化差不多。而且,函数内联不同于常量内联,函数体通常比常量复杂,函数内联会导致函数体被拷贝到每个调用处,如果函数体比较大而被调用处又比较多,就会导致编译出的字节码变大很多,反而会造成负优化。
那么,inline
的作用到底是什么呢?事实上,inline 关键字不止可以内联自己的内部代码,还可以内联自己内部的内部的代码。内部的内部就是自己函数类型的参数。
高阶函数存在的问题
1 | fun hello(action: () -> Unit) { |
因为 Java 没有对函数类型变量的原生支持,Kotlin 需要想办法让自己新引入的概念在 JVM 中落地。它想的就是用一个 JVM 对象作为函数类型变量的实际载体。也就是说上面的代码,程序每次执行 hello 方法的时候都会创建一个对象来执行 Lambda 里面的代码,虽然这个对象很快就会被抛弃了,但还是被创建了。
一般情况下,这并不会有坏处,但是如果放在下面的循环中执行:
1 | fun main() { |
占用的内存瞬间起飞,并且函数是我们创建的,我们不知道、也不能规定别人在什么地方调用这个函数。也就是说这个函数是否出现在循环或者界面刷新之类的高频场景,是完全不可控的。这个时候才是 inline
该出场的时刻,上面提到 inline
可以内联内部的内部,也就是说上面的代码实际编译如下:
1 | inline fun hello(action: () -> Unit) { |
这就是 inline
关键字的作用:高阶函数有着天然的性能缺陷,我们通过让函数用内联的方式进行编译,来减少参数对象的创建,从而避免出现性能问题。
noinline
noinline
意思是不内联,不过它不是作用于函数,而是作用于函数的参数:对于一个标记了 inline
的内联函数,你可以对它的任何一个或者多个添加 noinline
关键字。
看看下面的例子:
1 | inline fun hello(action: () -> Unit): () -> Unit { |
Android Studio 中对这种情况会拒绝编译,这是因为由于我们使用 inline
对函数进行了修饰,导致函数类型参数 action
不再是对象。换句话说,对于编译后的字节码来说,这个对象根本就不存在,一个不存在的对象怎么使用?如果真的要使用的话,可以使用 noinline
修饰这个函数类型参数。
1 | inline fun hello(noinline action: () -> Unit): () -> Unit { |
上面的代码实际编译的结果如下:
1 | fun main() { |
所以, noinline
的作用是什么?是用来局部的、指向性的关掉函数的内联优化。既然是优化,为什么要关闭?因为这种优化会导致函数类型的参数无法被当做参数使用,也就是说这种优化会对 Kotlin 的功能做出一定程度的收窄。当需要这个功能的时候,就要手动关闭优化了。这也是 inline
默认是关闭、需要手动开启的另一个原因:它会收窄 Kotlin 的功能。
那么,什么时候该使用 noinline
,当你在 inline
修饰的函数中做一些风骚操作,Android Studio拒绝编译的时候,加上 noinline
就好。
crossinline
Kotlin 制定了一条规则:Lambda 中不允许使用 return,除非这个 Lambda 是内联函数的参数
我们先来看看下面这个例子:
1 | inline fun hello(action: () -> Unit) { |
Lambda 中调用的 return 会结束那个函数的执行,最外面的 hello,还是更外面一层的 main。这个问题,我们直接查看实际编译后的代码就可以知道了:
1 | fun main() { |
这样一看,可以知道这个 return 会结束 main 函数的执行。但是这就有一个问题了,难道每次我在 Lambda 中使用 return 都需要回到原函数中看看有没有使用 inline
关键字,才能确定 return 的结果吗?
这种不一致性给我们带来了极大的困扰,因此 Kotlin 才制定了上面的规则。这样的话使用就很简单了:
- Lambda 正常情况下是不能使用 return 的
- Lambda 可以使用 return 的情况,说明它结束的是外层再外层的函数
这样我们就可以引申出下面的问题:
1 | inline fun hello(action: () -> Unit) { |
我们的 return 本来是要结束最外层再外层函数的调用的,但是由于 Runnable
的存在,导致它无法结束。
这表示什么? 当内联函数的 Lambda 参数在函数内部是间接调用的时候,Lambda 里面的 return 会无法按照预期的行为进行工作。这就比较严重了,Kotlin 针对这种情况是不允许的,你会发现这段代码在 Android Studio 中是拒绝编译的。如果你真的有这个需求,那么就可以使用 crossinline
,对它解除这个限制。
但是,使用 crossinline
又会出现 return 到底结束那个函数的问题,Kotlin 的选择是不允许使用 return。也就是说 Lambda 的 return 和 函数类型参数的间接调用你只能选一个。
什么时候使用 crossinline
,当你需要突破内联函数的「不能间接调用参数」的限制的时候。其实,这个也不需要你去判断,当 Android Studio 报错的时候加上即可。