高阶函数
首先来看下kotlin里的高阶函数定义:如果一个函数接收另一个函数作为参数,或返回类型是一个函数类型,那么该函数被称为是高阶函数。
比如下面的代码:
private fun highFuc(name: String, block: (String) -> Unit) {
block(name)
}
其中highFuc
是函数名,函数中传入了2个参数,第一个参数为String
类型,第二个参数即是函数类型,->左边的部分用来声明该函数接收什么参数的,多个参数之间用逗号隔开,如果没有参数直接使用()表示就可以了;->右边表示该函数的返回值是什么类型,如果没有返回值直接使用Unit
即可。
内联函数
内联函数,顾名思义,就是在编译时将作为函数参数的函数体直接映射到函数调用处,直接用一个例子来说明:
fun requestInfo() {
getStr()
}
fun getStr() {
println("inline")
}
很简单,getStr()中打印了一个字符串,然后requestInfo()中调用了getStr()函数,将上述代码转换成java代码之后:
public final void requestInfo() {
this.getStr();
}
public final void getStr() {
String var1 = "inline";
System.out.println(var1);
}
继续,在getStr()的前面加上inline声明,如下:
fun requestInfo() {
getStr()
}
//普通函数中并不推荐加inline关键字
inline fun getStr() {
println("inline")
}
转换成java之后:
public final void requestInfo() {
String var3 = "inline";
System.out.println(var3);
}
可以看到转换成java之后的代码有明显的区别:加上inline之后,getStr()中的函数内容直接“复制粘贴”到requestInfo()中,即内联到函数调用处了。
inline
通过上面的例子,inline的作用就很明显了,就是在编译时直接将函数内容直接复制粘贴到调用处。
我们知道函数调用最终是通过JVM操作数栈的栈帧完成的,每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程,使用了inline关键字理论上可以减少一个栈帧层级。
那么是不是所有的函数前面都适合加上inline关键字了呢?答案是否定的,其实JVM本身在编译时,就支持函数内联,并不是kotlin中特有的,那么kotlin中什么样的函数才需要使用inline关键字呢?答:高阶函数!
只有高阶函数中才需要inline去做内联优化,普通函数并不需要,如果在普通函数强行加上inline,编辑器会立刻提醒:
Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types
意思是 内联对普通函数性能优化的预期影响是微不足道的。内联最适合带有函数类型参数的函数
为什么高阶函数要使用inline
inline优化了什么问题呢?因为我们使用的Lambda表示式在编译转换后被换成了匿名类的实现方式。
fun requestInfo() {
highFuc("inline") { str ->
println(str)
}
}
fun highFuc(name: String, block: (String) -> Unit) {
block(name)
}
转换成java之后:
public final void requestInfo() {
this.highFuc("inline", (Function1)null.INSTANCE);
}
private final void highFuc(String name, Function1 block) {
block.invoke(name);
}
public interface Function1<in P1, out R> : Function<R> {
public operator fun invoke(p1: P1): R
}
所以函数参数最终会转换成interface,并通过创建一个匿名实例来实现。这样就会造成额外的内存开销。为了解决这个问题,kotlin引入inline内联功能,将Lambda表达式带来的性能开销消除。还是上面的例子,这次我们对高阶函数添加inline关键字:
fun requestInfo() {
highFuc("inline") { str ->
println(str)
}
}
//注意:这里添加了inline关键字
inline fun highFuc(name: String, block: (String) -> Unit) {
block(name)
}
转换成java之后:
public final void requestInfo() {
String name$iv = "inline";
System.out.println(name$iv);
}
noinline
当函数被inline标记时,使用noinline可以使函数参数不被内联。
fun requestInfo() {
highFuc({
println("noinline")
}, {
println("inline")
})
}
//highFuc被inline修饰,而函数参数block0()使用了noinline修饰
inline fun highFuc(noinline block0: () -> Unit, block1: () -> Unit) {
block0()
block1()
}
转换成java之后:
public final void requestInfo() {
Function0 block0$iv = (Function0)null.INSTANCE;
block0$iv.invoke();
String var5 = "inline";
System.out.println(var5);
}
结果也很明显,block0()函数没有被内联,而block()函数被内联,这就是noinline的作用。
如果想在非内联函数Lambda中直接return怎么办?比如我想这么写:
fun requestInfo() {
highFuc {
return //错误,不允许在非内联函数中直接return
}
}
fun highFuc(block: () -> Unit) {
println("before")
block()
println("after")
}
对不起,不允许!会直接在return
的地方报'return' is not allowed here错误。 但是可以写成return@highFuc
,即:
fun requestInfo() {
highFuc {
return@highFuc //正确,局部返回
}
}
fun highFuc(block: () -> Unit) {
println("before")
block()
println("after")
}
其中return
是全局返回,会影响Lambda之后的执行流程;而return@highFuc
是局部返回,不会影响Lambda之后的执行流程。如果我就想全局返回,那么可以通过inline来进行声明:
fun requestInfo() {
highFuc {
return
}
}
inline fun highFuc(block: () -> Unit) {
println("before")
block()
println("after")
}
因为highFuc
通过inline
声明为内联函数,所以调用方可以直接使用return
进行全局返回,执行requestInfo()
的结果:
before
可以看到Lambda
之后的after
并没有被执行,因为是全局返回,当然可以改成return@highFuc
局部返回,这样就可以都执行了。
结论:内联函数所引用的Lambda表达式中是可以使用return关键字来进行函数返回的,而非内联函数只能进行局部返回。
现在有一种场景,我既想使用inline
优化高阶函数,同时又不想调用方打断我的执行流程(因为inline是支持全局return的),貌似冲突了,这时候怎么办呢,这时候就需要crossinline
了。
crossinline
允许inline内联函数里的函数类型参数可以被间接调用,但是不能在Lambda表达式中使用全局return返回。
fun requestInfo() {
highFuc {
return //错误,虽然是inline内联函数,但Lambda中使用crossinline修饰,所以不允许全局返回了
}
}
inline fun highFuc(crossinline block: () -> Unit) {
println("before")
block()
println("after")
}
crossinline
关键字就像一个契约,它用于保证内联函数的Lambda
表达式中一定不会使用return全局返回,这样就不会冲突了。当然return@highFuc
局部返回还是可以的。
总结
- inline:编译时直接将函数内容直接复制粘贴到调用处。
- noinline:当函数被inline标记时,使用noinline可以使函数参数不被内联。
- crossinline: 允许内联函数里的函数类型参数可以被间接调用,但是不能在Lambda表达式中使用全局return返回
参考
【1】https://juejin.cn/post/6869954460634841101
【2】重学 Kotlin —— inline,包治百病的性能良药?