本文基于Kotlin 1.7.0版本,目前Kotlin最新版本为1.8.22
相信大家在日常使用Kotlin的过程中,已经接触了很多inline
函数,包括源码中也有很多很多方法使用了inline
来修改某些方法,不知道是不是有种疑问,一个方法明明可以直接调用,为啥非要用inline
来修饰呢?inline
修改的方法参数中,竟然还有noinline
和crossinline
关键字来修饰lambda
。下面来详细说明下这三个关键字的作用和使用场景。
inline内联
被inline
修饰的方法叫做内联函数,它修饰的方法需要接收了一个或多个lambda
表达式作为参数,
如果此方法参数没有lambda
表达式,那么编译器将提醒你Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types
,这个警告说明:此内联对性能的影响很小、微乎其微,内联适合的是具有函数类型的参数,所以编译器觉得此方法不适用于inline
修饰。
我们来通过inline
和非inline
函数对比一下它的作用,两个方法实现相同的功能,然后看下最终编译器是如何调用两种函数。
// 此函数为内联函数,传入一个lambda表达式
inline fun inlineTest(block: () -> Unit) {
println("inlineTest")
block()
}
// 此函数为非内联函数,也是传入一个lambda表达式
fun test(block: () -> Unit) {
println("test")
block()
}
两个函数除了inline
关键字修饰不同意外,其余都是一致的,接下来看看调用之后的输出是否一致,此处我们加上了测量函数执行时间的功能,以此更直观的观察inline
带来的效果。
fun main() {
println(measureTimeMillis {
inlineTest {
println("main inlineTest")
}
})
println(measureTimeMillis {
test {
println("main test")
}
})
}
#
inlineTest
main inlineTest
1
test
main test
9
inline函数执行时间为1,非inline函数执行时间为9
两个函数执行的效果是一致的,但是从输出日志中可以暗处,inline
函数的执行时间要明显比非inline
函数少,这也就是为什么官方推荐我们在有lambda
参数的时候加上inline
使其变为内联函数,那么inline
到底有了什么魔方可以减少了函数调用的开销呢,下面通过将class
反编译成Java
来看看函数的具体调用。
public final class MainKt {
public static final void main() {
// ①此处为inline函数的调用过程
long start$iv = System.currentTimeMillis();
System.out.println("inlineTest");
System.out.println("main inlineTest");
long var6 = System.currentTimeMillis() - start$iv;
System.out.println(var6);
// ②此处为非inline函数的调用过程
start$iv = System.currentTimeMillis();
test((Function0)null.INSTANCE);
var6 = System.currentTimeMillis() - start$iv;
System.out.println(var6);
}
// ③在编译器中可以看到此处inline函数并没有调用的地方
public static final void inlineTest(@NotNull Function0 block) {
Intrinsics.checkNotNullParameter(block, "block");
int $i$f$inlineTest = false;
System.out.println("inlineTest");
block.invoke();
}
public static final void test(@NotNull Function0 block) {
Intrinsics.checkNotNullParameter(block, "block");
System.out.println("test");
block.invoke();
}
// $FF: synthetic method
public static void main(String[] args) {
main();
}
}
在反编译的代码中标记了三处地方,顺着这三处就可以清晰的看出inline
函数和常规函数的不同之处。
第一处①是inline
函数的具体调用过程,从代码中可以看出,此处并没有直接调用inlineTest()
这个方法,反而是直接将函数的内容拷贝到调用处,并且将lambda
中的代码也一并拷贝过来了,直接减少了函数调用的开销;
第二处②是常规函数的调用过程,它是调用了test()
方法,并且传入了一个Function
对象,这个Function0
就是我们lambda
表达式
第三处③是需要在编译器中才能看出效果,在编译器中,我们可以看出teinlineTest()
函数并没有调用者,而test()
函数是在main()
函数中有调用的地方。
这样我们就可以直观的感受到,inline
修饰的函数也就是内联函数在调用的时候并非直接调用此函数本身,而是将函数内的代码直接拷贝到调用处。这样带来优势就是:减少函数调用带来的开销,提高程序的性能;消除lambda表达式带来的额外开销,避免创建额外的对象。
noinline
上面我们了解了inline
内联函数的使用和优势,接着我们看一下配合inline
使用的noinline
,看名字大致可以猜测到,noinline
就是非内联的意思,也就是表明被noinline
修饰的参数强制不允许内联,此参数作为一个普通的函数引用传递,并且noinline
必须搭配inline
使用。下面还是通过代码来直观感受下noinline
的作用。
fun main() {
noinlineTest({
println("main inline")
}, {
println("main noInline")
})
}
// 定义一个内联函数,第一个参数可内联使用,第二个参数使用noinline修饰,强制不内联
inline fun noinlineTest(block: () -> Unit, noinline no: () -> Unit) {
println("noinlineTest")
block()
no()
}
# log
noinlineTest
main inline
main noInline
noinlineTest({},{})
函数为一个内联函数,两个lambda
参数唯一不同的就是第二个参数被noinline
修饰了,从log
中可以看出,输出的信息在我们意料之中,也并不能看出noinline
带来的不同之处,我们还是得反编译看下生成的代码到底变化了什么。
public final class NoinlineKt {
public static final void main() {
// ① no参数为一个Function,直接实例化了
Function0 no$iv = (Function0)null.INSTANCE;
// ② noinlineTest函数中打印的日志
System.out.println("noinlineTest");
// ③ block函数内代码直接拷贝到这
System.out.println("main inline");
// ④ 执行no参数具体的代码
no$iv.invoke();
}
public static final void noinlineTest(@NotNull Function0 block, @NotNull Function0 no) {
Intrinsics.checkNotNullParameter(block, "block");
Intrinsics.checkNotNullParameter(no, "no");
int $i$f$noinlineTest = false;
System.out.println("noinlineTest");
block.invoke();
no.invoke();
}
// $FF: synthetic method
public static void main(String[] args) {
main();
}
}
反编译的代码中我们注释了四处地方,分别介绍了内联参数和noinline
参数的执行步骤:
第一处①先实例化一个no
参数,Kotlin的lambda
在Java
中对应的是Function
对象;
第二处②直接拷贝内联函数noinlineTest()
的println()
方法输出日志;
第三处③直接拷贝内联参数的输出日志方法,从此处可以看到block
参数被内联了,它被拷贝到调用处;
第四处④执行了noinline
参数的内部代码
从上面反编译的代码我们可以得出,noinline
修饰的参数被强制非内联了,它还是会去调用内部的代码,而非直接拷贝内部代码到调用处,这就是noinline
关键字的作用。
crossinline
crossinline
相对于前面inline
和noinline
来说,它使用的地方较少,个人的理解它的意思为强制内联的意思,它表示被修饰的lambda
参数强制执行内联作用,一般我们见到的使用它的地方都是在内联函数中使用了lambda
表达式,并且在此表达式调用了内联函数的lambda
参数,此时如果不使用crossinline
修饰参数,编译器会报错,下面我们通过代码来说明
inline fun crossinlineTest(block: () -> Unit, crossinline cross: () -> Unit) {
println("crossinlineTest")
thread {
// 编译器会在此处报错:Can't inline 'block' here: it may contain non-local returns. Add 'crossinline' modifier to parameter declaration 'block'
block()
cross()
}
}
上面就是一个内联函数使用crossinline
的实例,我们在内联函数中使用thread{}
开启一个线程,并在新的线程中调用block()
和cross()
参数的执行,此时编译器在block()
调用处就直接报错,告诉我们block
在此种场景下不能直接内联,它有可能包含了非本地的return
,这样我们就需要加上crossinline
来修饰参数,从cross
调用的情况就说明了它可以正常执行。
编译器为什么不允许我们在thread{}
中直接执行block()
参数呢?
因为内联函数在调用的时候是直接将代码拷贝到调用处的,所以存在block()
中直接return
的情况,他会执行返回到调用处并且不再执行调用处后续的代码,看个具体代码理解一下:
inline fun inlineReturn(block: () -> Unit) {
block()
}
fun main() {
inlineReturn {
println("start")
return
// 此处将不会执行
println("end")
}
// 此处也不会执行
println("main")
}
# log
start
看上面代码,我们直接在inlineReturn
函数的block()
中使用return
来返回,结果它并非退出到内联函数,而是直接退出了main()
函数,到这我们记住inline
函数是可以直接使用return
来做出返回操作。
下面我们再看看crossinline
关键字的作用:
inline fun crossinlineReturn(crossinline block: () -> Unit) {
block()
}
fun main() {
crossinlineReturn {
println("start")
// 此处编译器会直接报错
return
println("end")
}
println("main")
}
crossinlineReturn
和上面inlineReturn
函数基本一致,只是block
参数使用了crossinline
修饰,此时我们如果还想使用return
来操作返回,编译器会直接给出报错提示,告诉我们此时不可以使用return
,它需要指定返回的目的地,需要采用return@crossinlineReturn
这样的形式,告诉编译器只是退出到内联函数,并非直接退出main()
函数,而且它的输出为:
# log
start
main
通过crossinline
就可以禁止在内联的lambda
表达式中使用return
操作了。
好了,到这为止我们已经将inline
、noinline
和crossinline
三者的关系及其用法、效果都介绍完了,如果你有收获帮忙点个关注吧,欢迎评论区输出不同的看法和认为不正确的地方,ths!