用编译时常量的概念,引出本文要讲内联函数inline:
1.编译时常量
- Java的编译时常量 Compile-time Constant
它有四个要求:1.这个变量需要是 final 的 2.类型只能是字符串或者基本类型 3.这个变量需要在声明的时候就赋值 4.等号右边还不能太复杂
final String name = "hsf";
final int age = 18;
final long current = System.currentTimeMillis();
这种编译时常量,会被编译器以内联的形式进行编译,也就是直接把你的值拿过去替换掉调用处的变量名来编译。这样一来,程序结构就变简单了,编译器和 JVM 也方便做各种优化。这,就是编译时常量的作用。
- 这种编译时常量,到了 Kotlin 里有了一个专有的关键字,叫 const
一个变量如果以 const val 开头,它就会被编译器当做编译时常量来进行内联式编译:
当然你得符合编译时常量的特征啊,不然会报错,不给编。
让变量内联用的是 const;而除了变量,Kotlin 还增加了对函数进行内联的支持。在 Kotlin 里,你给一个函数加上 inline 关键字,这个函数就会被以内联的方式进行编译。
但!虽然同为内联,inline 关键字的作用和目的跟 const 是完全不同的
2.inline出现的原因
事实上,inline 关键字不止可以内联自己的内部代码,还可以内联自己内部的内部的代码。什么叫「内部的内部」?就是自己的函数类型的参数
声明一个函数,其有一个函数类型的参数
我可以填成匿名函数的形式:
也可以简单点,写成 Lambda 表达式:
因为 Java 并没有对函数类型的变量的原生支持,Kotlin 需要想办法来让这种自己新引入的概念在 JVM 中落地,就是用一个 JVM 对象来作为函数类型的变量的实际载体,让这个对象去执行实际的代码。
也就是说,在我对代码做了刚才那种修改之后,程序在每次调用 hello() 的时候都会创建一个对象来执行 Lambda 表达式里的代码,虽然这个对象是用一下之后马上就被抛弃,但它确实被创建了。
但是如果这种函数被放在循环里执行,内存占用一下就飚起来了。而且关键是,你作为函数的创建者,并不知道、也没法规定别人在什么地方调用这个函数,也就是说,这个函数是否出现在循环或者界面刷新之类的高频场景里,是完全不可控的。
3.inline的作用
函数在被加了 inline 关键字之后,编译器在编译时不仅会把函数内联过来,而且会把它内部的函数类型的参数也内联过来。换句话说,这个函数被编译器贴过来的时候是完全展开铺平的:
经过这种优化,就避免了函数类型的参数所造成的临时对象的创建。这样的话,就不怕在循环或者界面刷新这样的高频场景里调用它们了
这就是 inline 关键字的用处:高阶函数(Higher-order Functions)有它们天然的性能缺陷,我们通过 inline 关键字让函数用内联的方式进行编译,来减少参数对象的创建,从而避免出现性能问题
4.noinline
当一个函数被内联之后,它内部的那些函数类型的参数就不再是对象了,因为它们的壳被脱掉了。换句话说,对于编译之后的字节码来说,这个对象根本就不存在。一个不存在的对象,你怎么使用?
所以当你要把一个这样的参数当做对象使用的时候,Android Studio 会报错,告诉你这没法编译:
那……我如果真的需要用这个对象怎么办?加上 noinline:
加了 noinline 之后,这个参数就不会参与内联了:
noinline 的作用:用来局部地、指向性地关掉函数的内联优化的。使得函数中的函数类型的参数可能被当做对象来使用
5.在Lamdba中使用return
//情况一
fun hello(postAction: () -> Unit) {
LogUtil.d("Hello!")
postAction()
}
//情况二
inline fun hello(postAction: () -> Unit) {
LogUtil.d("Hello!")
postAction()
}
hello {
LogUtil.d("Bye!")
return //实际上对于情况一,这里编译过不了
LogUtil.d("Bye!2")
}
对于函数参数中的Lambda 的 return,我们有这样的直观感受。如果是在非内联函数中,return的应该是hello;如果是在内联函数中,return的应该是hello的外层函数。这就造成了一种歧义,那我一个 return 结束哪个函数,竟然要看这个函数是不是内联函数!那岂不是我每次写这种代码都得钻到原函数里去看看有没有 inline 关键字,才能知道我的代码会怎么执行?那这也太难了吧!
为了消除在Lamdba中return所带来的歧义,Kotlin指定了Lambda中return的规则:
- 规则1、只有内联函数的 Lambda 参数可以使用 return。
fun hello(postAction: () -> Unit) {
LogUtil.d("Hello!")
postAction()
}
hello {
LogUtil.d("Bye!")
return //编译不通过,提示return' is not allowed here
LogUtil.d("Bye!2")
}
注意:如果给函数参数又加上了noinline,那么lambda中的return又报错了,很简单,因为它不属于内联的参数了,它又不是铺平的了,此时它的return又变得有歧义了
- 规则2、Lambda 里的 return,结束的不是直接的外层函数,而是外层再外层的函数(因为内联函数已经被铺平了)
inline fun hello(postAction: () -> Unit) {
LogUtil.d("Hello!")
postAction()
}
hello {
LogUtil.d("Bye!")
return //编译通过,这个return结束的是hello外层的函数
LogUtil.d("Bye!2")
}
注意:非Lamdba,也就是那种用fun来写的函数类型参数,在是否内联函数中都可以使用return,因为它都结束的是自己的这个fun
fun hello(postAction: () -> Unit) {
LogUtil.d("Hello!")
postAction()
}
hello(fun() {
LogUtil.d("Bye!")
return //这个return结束的是hello
LogUtil.d("Bye!2")
})
6.crossinline
如果我要对内联函数里的函数类型的参数进行间接调用,例如:
fun ppp(runnable: Runnable) {
...
}
inline fun hello(postAction: () -> Unit) {
LogUtil.d("Hello!")
ppp{
//实际这里编译不通过,提示: Can't inline 'postAction' here: it may contain non-local returns. Add 'crossinline' modifier to parameter declaration 'postAction'
postAction()
}
}
fun main(){
hello {
LogUtil.d("Bye!")
return
LogUtil.d("Bye!2")
}
}
这就带来了一个麻烦:本来在调用处行的 return 是要结束它外层再外层的 main() 函数的,但现在因为它被放在了 ppp() 里,hello() 对它的调用就变成了间接调用。所谓间接调用,直白点说就是它和外层的 hello() 函数之间的关系被切断了。和 hello() 的关系被切断,那就更够不着更外层的 main() 了,也就是说这个间接调用,导致 Lambda 里的 return 无法结束最外面的 main() 函数了。
因此Kotlin选择了,干脆内联函数里的函数类型的参数,不允许这种间接调用。
那我如果真的有这种需求呢?如果我真的需要间接调用,怎么办?使用 crossinline。crossinline 也是一个用在参数上的关键字。当你给一个需要被间接调用的参数加上 crossinline,就对它解除了这个限制,从而就可以对它进行间接调用了:
inline fun hello(crossinline postAction: () -> Unit) {
LogUtil.d("Hello!")
ppp{
postAction()
}
}
不过这就又会导致前面说过的return歧义的问题,它结束的是谁?是包着它的 ppp(),还是依然是hello的外层?
hello {
LogUtil.d("Bye!")
return
LogUtil.d("Bye!2")
}
对于这种不一致,Kotlin 增加了一条额外规定:内联函数里被 crossinline 修饰的函数类型的参数,将不再享有「Lambda 表达式可以使用 return」的福利。所以这个 return 并不会面临「要结束谁」的问题,而是直接就不许这么写。
fun ppp(runnable: Runnable) {
}
inline fun hello(crossinline postAction: () -> Unit) {
LogUtil.d("Hello!")
ppp{
postAction()
}
}
fun main(){
hello {
LogUtil.d("Bye!")
return //这里编译不通过,提示:'return' is not allowed here
LogUtil.d("Bye!2")
}
}
也就是说,间接调用和 Lambda 的 return,你只能选一个。
所以什么时候需要 crossinline?当你需要突破内联函数的「不能间接调用参数」的限制的时候,但伴随着就要放弃Lambda中使用return了
7.总结
- inline 可以让你用内联(也就是函数内容直插到调用处)的方式来优化代码结构,从而减少函数类型的对象的创建;
- noinline 是局部关掉这个优化,来摆脱 inline 带来的「不能把函数类型的参数当对象使用」的限制;
- crossinline 是局部加强这个优化,让内联函数里的函数类型的参数可以被间接调用
8.扩展:inline的另类用法,在函数里直接去调用 Java 的静态方法
用偷天换日的方式来去掉了这些 Java 的静态方法的前缀,让调用更简单:
这种用法不是 inline 被创造的初衷,也不是 inline 的核心意义,这属于一种相对偏门的另类用法。不过这么用没什么问题啊,因为它的函数体简洁,并不会造成字节码膨胀的问题。你如果有类似的场景,也可以这么用。
参考文章:
Kotlin 源码里成吨的 noinline 和 crossinline 是干嘛的?看完这个视频你转头也写了一吨