【Kotlin】inline&crossinline&noinline关键字

本文介绍了Compose的重组流程,主要是最小重组范围的界定和优化
来自扔物线朱凯大佬的博客学习笔记
JVM常量编译时优化
Kotlin中,使用了 const val 关键字修饰的变量,在编译时会被视为常量,并且在编译时进行了优化。直接将其值复制到调用处,而不是像普通变量一样在运行时进行变量访问。这可以提高代码的执行效率,因为避免了变量调用的开销。
const val CONST_VAL = 10
fun main() {
println(CONST_VAL)
}
// 编译后
fun main() {
println(10)
}
inline 内联函数
编译时同样被提前处理的还有内联函数,即使用了 inline 关键字修饰的函数。
JVM在编译时,会将inline函数内的代码直接复制到调用处,而不是像普通函数一样在运行时进行函数调用。听起来可能会对性能有优化,实际上少一层函数调用栈的优化是非常微小的。
而同时, 函数内联 不同于 常量内联 的地方在于,函数体通常比常量复杂多了,而函数内联会导致函数体被拷贝到每个调用处,如果函数体比较大而被调用处又比较多,就会导致编译出的字节码变大很多。
lambda参数实现方式
在Kotlin中,lambda参数的实现方式是使用了 匿名内部类 ,而不是使用了 函数指针 。
在编译之后,可以看到lambda参数调用的地方,实际上是Kotlin帮我们生成了一个匿名内部类,然后在调用处调用这个匿名内部类的方法。
class LambdaTest {
fun testInline(lambdaParams:()->Unit) {
lambdaParams()
}
}
经过反编译成Java代码之后:
public final class LambdaTest {
@NotNull
public final LambdaTest testInline(@NotNull Function0 lambdaParams) {
Intrinsics.checkNotNullParameter(lambdaParams, "lambdaParams");
lambdaParams.invoke();
return this;
}
}
可以看到,lambdaParams的类型是 Function0 ,这是一个接口。在运行过程中,就会生成一个匿名内部类,然后在调用处调用这个匿名内部类的方法。
inline对lambda的优化
如果上述的testinline方法,在外部被高频循环调用。
fun main() {
val lambdaTest = LambdaTest()
for (i in 0..100000) {
lambdaTest.testInline {
println("hello world")
}
}
}
内存占用会蹭的一下涨上来。
如果使用了这个接收lambda参数的方法使用了 inline 关键字修饰,就不会生成匿名内部类,而是直接将lambda的代码块里面的代码复制到调用处。
inline 关键字不止可以内联自己的内部代码,还可以内联自己内部的内部的代码,意思是什么呢,就是你的函数在被加了 inline 关键字之后,编译器在编译时不仅会把函数内联过来,而且会把它内部的函数类型的参数——那就是那些 Lambda 表达式——也内联过来。换句话说,这个函数被编译器贴过来的时候是完全展开铺平的:
kotlin源代码:
class LambdaTest {
inline fun testInline(lambdaParams:()->Unit) {
lambdaParams()
}
}
fun main() {
val lambdaTest = LambdaTest()
for (i in 0..100000) {
lambdaTest.testInline {
println("hello world")
}
}
}
反编译之后:
public final class LambdaTest {
public final void testInline(@NotNull Function0 lambdaParams) {
Intrinsics.checkNotNullParameter(lambdaParams, "lambdaParams");
lambdaParams.invoke();
}
}
public final class MainKt {
public static final void main() {
LambdaTest lambdaTest = new LambdaTest();
int $i$iv = 0;
int var3;
for(var3 = 100000; $i$iv <= var3; ++$i$iv) {
System.out.println("hello world");
}
}
}
高阶函数(Higher-order Functions)有它们天然的性能缺陷,我们通过 inline 关键字让函数用内联的方式进行编译,来减少参数对象的创建,从而避免出现性能问题。
inline另类用法
在kotlin的 UMath.kt 工具类中,有一个max方法:
@SinceKotlin("1.5")
@WasExperimental(ExperimentalUnsignedTypes::class)
@kotlin.internal.InlineOnly
public inline fun max(a: UInt, b: UInt): UInt {
return maxOf(a, b)
}
这个maxOf方法,来自于另一个工具类 UComparisonsKt :
@SinceKotlin("1.5")
@WasExperimental(ExperimentalUnsignedTypes::class)
public fun maxOf(a: UInt, b: UInt): UInt {
return if (a >= b) a else b
}
这里就通过内联的方式,将maxOf方法的代码块内联到了调用处。
可以直接通过方便的顶层函数的方式,来使用工具类,不需要创建实例或者带外部类名。
noinline
inline 是内联,而 noinline 就是不内联。不过它不是作用于函数的,而是作用于函数的参数:对于一个标记了 inline 的内联函数,你可以对它的任何一个或多个函数类型的参数添加 noinline 关键字。添加了之后,这个参数就不会参与内联。
函数类型的参数,它本质上是个对象。我们可以把这个对象当做函数来调用,这也是最常见的用法。但同时我们也可以把它当做对象来用。比如把它当做返回值:
inline fun testInline(lambdaParams:()->Unit) {
lambdaParams()
return lambdaParams
}
但当我们把函数进行内联的时候,它内部的这些参数就不再是对象了,因为他们会被编译器拿到调用处去展开。
当一个函数被内联之后,它内部的那些函数类型的参数就不再是对象了,因为它们的壳被脱掉了。换句话说,对于编译之后的字节码来说,这个对象根本就不存在。一个不存在的对象,你怎么使用?
所以当你要把一个这样的参数当做对象使用的时候,Android Studio 会报错,告诉你这没法编译
noinline 就是用来局部地、指向性地关掉函数的内联优化的。既然是优化,为什么要关掉?因为这种优化会导致函数中的函数类型的参数无法被当做对象使用,也就是说,这种优化会对 Kotlin 的功能做出一定程度的收窄。而当你需要这个功能的时候,就要手动关闭优化了。这也是 inline 默认是关闭、需要手动开启的另一个原因:它会收窄 Kotlin 的功能。
crossinline
当 inline 函数将 Lambda 参数传递给另一个执行上下文(如另一个函数、另一个线程、协程或其他作用域)时,为了防止非局部返回,必须使用 crossinline。
保持 Lambda 的内联优化,但禁止在 Lambda 内部使用裸奔的 return 关键字(即非局部返回)。它确保 Lambda 只能使用标签返回 (return@label) 或隐式返回。使用 crossinline 确保内联函数的行为符合预期,避免 Lambda 内部的 return 意外地跳出外部的非内联函数。
看这样一个情景:
一个内联函数,接受一个 lambda 参数。
inline fun lambdaReturnTest(insertAction: () -> Unit) {
insertAction()
}
如果在调用处,lambda参数里带一个return:
override fun onCreate() {
super.onCreate()
Log.i("sdvgsrhbTAG", "before erftgyujhf")
lambdaReturnTest {
println("Hello World")
return
}
Log.i("sdvgsrhbTAG", "after erftgyujhf")
}
这时候结束的不是这个lambdaReturnTest方法,而是onCreate方法。因为lambdaReturnTest方法被内联了,会直接铺平展开到调用处,连带里面的return。
这样的话,我们每次在lambda里面使用return还需要确认这个函数是否是内联函数,才可以确认这个return结束的是哪一个函数。为此Kotlin规定 不允许在lambda参数中使用return,除非这个使用lambda参数的函数是内联函数。
那这样的话规则就简单了:
- Lambda 里的 return,结束的不是直接的外层函数,而是外层再外层的函数;
- 但只有内联函数的 Lambda 参数可以使用 return。
目前的Kotlin版本其实也可以在return后面使用\@来指明返回的哪一级的函数。
示例:异步或嵌套执行
假设您有一个 safeRun 函数,它在一个内部(非内联)的 Runnable 中执行您的 Lambda。
// 内部非内联函数,它接受一个普通 Lambda/Runnable
fun executeInExecutor(block: () -> Unit) {
// 实际的 Android/Java 场景可能是:Executor.execute(Runnable { ... })
println("任务被包装并排队...")
block() // 模拟执行
}
// 场景:创建一个安全的执行块,但其中的任务会被传递到另一个函数中执行
inline fun safeRun(crossinline block: () -> Unit) {
println("--- 准备执行 ---")
// 如果这里没有 crossinline,编译器无法保证 block() 不会被非局部返回跳出 safeRun 之外
executeInExecutor {
// block 的代码在这里被执行
block()
}
println("--- 执行完毕 ---")
}
fun main() {
fun callSafeRun() {
safeRun {
println("开始任务")
// return // ❌ 编译错误:禁止非局部返回
return @ safeRun // ✅ 允许:只能使用标签返回,只跳出 safeRun
}
println("callSafeRun 结束")
}
callSafeRun()
}
/* 输出:
--- 准备执行 ---
任务被包装并排队...
开始任务
--- 执行完毕 ---
callSafeRun 结束
*/
如果没有 crossinline,Lambda { return } 理论上可以执行非局部返回,直接跳出 callSafeRun 函数。但由于 Lambda 实际是在非内联的 executeInExecutor 内部执行的,这种行为是不允许的,因此 crossinline 强制阻止了非局部返回,以保证程序的控制流是清晰且安全的。
双层嵌套的lambda场景
inline fun lambdaReturnTest(insertAction: () -> Unit) {
doubleLambda { insertAction() }
}
fun doubleLambda(insertAction: () -> Unit) {
insertAction()
}
doubleLambda方法是一个普通函数,非内联函数,它的参数是一个函数类型的参数。
如果像这样带两层lambda调用,那么其中使用return就又会无法判断结束的到底是哪一层函数。 这里Kotlin是直接禁止了这种写法。
如果确实要有这种间接调用需求,那么可以使用crossinline来解决。当你给一个需要被间接调用的参数加上 crossinline,就对它进行了局部加强内联,相当于insertAction还是会被展开铺平到调用处,解除了这个限制,从而就可以对它进行双层间接调用了。
但是又会有return结束层级不确定性,所以Kotlin规定了使用了crossinline的函数,不能在lambda参数中使用return。
只能二选一了。
总结
结论就是:
- inline 可以让你用内联——也就是函数内容直插到调用处——的方式来优化代码结构,从而减少函数类型的对象的创建;
- noinline 是局部关掉这个优化,来摆脱 inline 带来的「不能把函数类型的参数当对象使用」的限制;
- crossinline 是局部加强这个优化,让内联函数里的函数类型的参数可以被当做对象使用。