
前几天和吴彦祖吃饭,聊到他最近在用 Kotlin 写一个 Android 项目。
饭吃到一半,他突然放下筷子问我:"我在标准库里看到很多函数都标了 inline,像 repeat()、let() 这些。我知道它跟性能有关,但它到底是怎么工作的?"
这个问题其实很有代表性------很多开发者知道 inline 能优化性能,但对它的完整能力了解有限。
所以借这篇文章,系统地聊一聊。
inline 关键字用于优化接受 lambda 参数的高阶函数,通过减少函数调用(尤其是涉及 lambda 表达式时)带来的运行时开销。
当一个函数标记为 inline 后,Kotlin 编译器会将该函数的函数体直接替换到每个调用处,从而无需为函数对象分配内存,也避免了 lambda 调用的开销。
如何工作
通常情况下,传递 lambda 作为参数需要创建一个对象来表示该 lambda,然后调用其方法。
这在性能敏感的场景中会带来额外开销。
将函数标记为 inline 则可以消除这种开销------编译器会把实际函数体和 lambda 代码直接插入调用位置。来看一个例子:
kotlin
inline fun performOperation(operation: () -> Unit) {
println("Starting operation...")
operation()
println("Operation completed.")
}
fun main() {
performOperation {
println("关注 RockByte 公众号")
}
}
这里 performOperation 被标记为 inline,编译器会把对 performOperation 的调用替换为其函数体(包括 lambda 代码),从而减少对象分配,提升运行时性能。
好处
让我们深入看看 inline 究竟能带来什么好处。如果不使用 inline,每次传递 lambda 给函数时,编译器都必须创建一个 Function 对象来承载该 lambda,这牵涉到:
- 对象分配:在堆上新建一个对象来保存 lambda 的代码。
- 内存开销:该对象占用内存。
- 虚方法调用 :调用 lambda 需要通过虚方法调用(
invoke()),比直接方法调用慢。
在频繁调用的函数中(尤其是循环内部),大量小型 Function 对象的创建开销会逐渐累积,给 GC 带来压力,最终影响整体性能。
inline 关键字彻底解决了这个问题。当函数被内联后,编译器不再为 lambda 创建 Function 对象,而是把 lambda 体直接嵌入调用处。
kotlin
// --- 非内联函数 ---
fun nonInlinedAction(block: () -> Unit) {
println("Before action")
block() // 这里是对 Function 对象的虚调用
println("After action")
}
// --- 内联函数 ---
inline fun inlinedAction(block: () -> Unit) {
println("Before action")
block() // lambda 中的代码会被复制到这里
println("After action")
}
fun main() {
nonInlinedAction { println("Executing non-inlined action") }
inlinedAction { println("Executing inlined action") }
}
// --- 编译器大致会生成这样的代码 ---
fun main_compiled() {
// nonInlinedAction:创建 Function 对象并调用函数
nonInlinedAction(Function0 { println("Executing non-inlined action") })
// inlinedAction:全部代码被直接复制,无对象,无方法调用
println("Before action")
println("Executing inlined action") // 无对象,无虚调用
println("After action")
}
inline 最核心的性能收益,就是消除对象创建和虚方法调用。
非局部返回

内联带来的另一个直接而强大的能力是非局部返回。
在普通的非内联 lambda 中,你只能用 return@label 退出 lambda 自身,而不能用裸 return 退出外层函数。
但由于内联 lambda 的代码被直接复制到了调用函数中,lambda 内的 return 语句表现得就像它原本就在调用函数中一样------它可以直接退出整个外层函数。
来看一段代码:
kotlin
inline fun forEach(numbers: List<Int>, action: (Int) -> Unit) {
for (number in numbers) {
action(number)
}
}
fun printFirstNegative(numbers: List<Int>) {
forEach(numbers) {
if (it < 0) {
// 非局部返回:这个 return 退出的是 printFirstNegative,而不仅仅是 lambda
println("找到第一个负数:$it")
return
}
}
// 如果 lambda 中触发了 return,这行及之后的代码都不会执行
println("遍历完成,没有找到负数")
}
fun main() {
printFirstNegative(listOf(3, 1, -2, 5))
// 输出:找到第一个负数:-2
// 注意:"遍历完成,没有找到负数" 不会被打印
}
这里的关键是:return 写在 lambda 的花括号 {} 里,但它退出的不是 lambda,而是整个外层函数 printFirstNegative。这就像你在 printFirstNegative 的函数体内直接写了 return 一样。
如果 forEach 不是 inline 函数,上面的代码将无法编译------普通 lambda 不允许用裸 return 退出外层函数,你只能用 return@forEach 来退出 lambda 自身。
这个特性让内联函数的行为更接近 for、while 这样的语言内建控制流结构------你可以用 return 随时跳出外层函数,就像在普通循环里写 return 一样自然。
reified:运行时可用的泛型
inline 解锁的另一大特性是 reified 类型参数。
在 JVM 上,泛型会在运行时被擦除(type erasure),正常情况下你无法在运行时拿到泛型参数 T 的类型(比如不能写 T::class)。
但当你把函数标记为 inline 并配合 reified 关键字,类型信息就能在运行时保留下来------这对类型检查和类型转换特别有用。
kotlin
inline fun <reified T> isInstance(value: Any): Boolean {
return value is T
}
fun main() {
println(isInstance<String>("Hello")) // true
println(isInstance<Int>("Hello")) // false
}
这里 reified 让 T 在运行时可以被用于 is T 这样的类型检查操作。
这在构建简洁、类型安全的 API 时非常实用,尤其是在库开发中。
Android 中一个典型场景就是按类型查找 Fragment:findFragment<MyFragment>()。
何时不该用
虽然 inline 能带来显著的性能和可读性收益,但它并非万能。不当或过度使用反而会适得其反。以下是应当避免使用 inline 的关键场景:
- 代码体积膨胀:内联意味着把函数体复制到每个调用处。小函数还好,大函数会造成代码膨胀和二进制体积增大,进而影响性能(比如更多的 CPU 缓存未命中)。
- 不适合大型函数:包含大量逻辑的函数最好保持非内联。在多个调用处重复大段代码会拖慢运行时效率,也会让 APK 更臃肿。
- 不可被重写 :内联函数在编译时直接替换,因此隐式地具有
final语义,子类无法重写。这限制了多态场景下的灵活性。 - 无 lambda 参数时意义不大 :如果函数不接受 lambda 参数,
inline避免 lambda 对象创建的核心优势就没了。这种情况下,JVM 的 JIT 编译器通常比开发者更擅长做内联优化。(不过,如果需要使用reified类型参数,即使没有 lambda 参数也必须使用inline。) - 不适用于递归和私有函数 :递归函数无法内联,编译器会直接报错。同样,如果私有函数不接受 lambda 参数,标记
inline的额外收益有限------JIT 编译器通常已经能自行优化。
小结
inline 关键字通过在调用处内联函数体和 lambda,优化了高阶函数的运行时性能,减少了内存开销。它还赋予了 reified 泛型等在运行时保留类型信息的能力。但使用时务必审慎------既要用它来提升性能,也要避免代码膨胀,保持代码的可维护性。
进阶:inline 属性
inline 属性 是使用了 inline 修饰符的属性,其 getter / setter 会在调用处被内联。
这意味着访问属性时不会产生方法调用开销------getter/setter 内的代码会在编译期直接替换到调用处。inline 属性特别适合逻辑简单的属性。
来看一个示例:
kotlin
inline var calculatedValue: Int
get() = someComplexCalculation() // getter 逻辑被内联
set(value) {
saveResult(value) // setter 逻辑被内联
}
当访问或修改 calculatedValue 时,getter/setter 代码会直接被替换到原位置,省去了方法调用的开销。
这样做,有如下的优势:
-
减少开销 :内联
getter/setter消除了方法调用的运行时开销,特别适合那种频繁访问、逻辑轻量的属性。 -
性能提升 :在频繁访问属性的关键代码路径中,
inline属性可以显著提升性能。 -
优化小逻辑 :非常适合计算属性、状态判断或轻量数据转换。例如:
kotlininline val isUserActive: Boolean get() = System.currentTimeMillis() - lastActivityTime < ACTIVE_THRESHOLD
这和 inline 函数的大部分优点是类似的。
这里 isUserActive 每次访问都直接内联计算,无需方法调用。
当然,inline 属性虽然好用,这种做法也有一些限制。
它只适合简单逻辑。复杂的 getter/setter 内联会导致字节码膨胀。另外,带有幕后字段(backing field)的属性不能标记为 inline,因为幕后字段无法被直接内联。
这个特性,实际上是一条关于属性访问器的 inline 提案引入的,该提案中说明允许在调用处内联属性访问器,从而减少方法调用开销。
这个提案使得那些没有幕后字段的轻量属性从中获益最大,这样做既能提升性能,又能在函数式编程模式中更高效地使用。
但该特性明确不涉及幕后字段或复杂操作,以避免字节码膨胀和复杂度增加。
简单总结一下。
inline 属性通过消除函数调用开销、将 getter/setter 逻辑直接嵌入调用位置来优化性能。它们最适合轻量级、频繁访问的属性------简洁高效。审慎地使用 inline 属性,可以写出既清晰又高性能的代码。
进阶:非 suspend 的 inline 函数也能接受挂起 lambda
标准库中的常用函数 ------ repeat()、map()、filter()。
这类标准库函数之所以能在 lambda 中接受挂起函数,是因为它们都被声明为 inline 函数。这使得它们能无缝地与挂起 lambda 配合,尽管它们自身签名并非协程感知。
内联函数允许编译器在编译期将函数体直接插入调用代码。当挂起 lambda 传递给内联函数时,编译器会将函数体(包括 lambda 代码)直接插入到调用处。由于调用处本身位于协程(suspend 函数)内部,lambda 中的挂起调用自然继承了该协程上下文,从而能正确执行。
来看一段代码:
kotlin
suspend fun printMessage(message: String) {
println("Message: $message")
}
suspend fun main() {
repeat(3) {
printMessage("skydoves $it")
}
}
这里 repeat() 是 inline 函数。虽然 repeat() 自身不是挂起函数,但编译器将其函数体内联到了调用代码中,使得挂起 lambda { printMessage(...) } 能在协程中正常执行。
为什么 inline 函数能做到这一点?
内联函数在让挂起 lambda 与协程无缝配合方面扮演了关键角色。以下是背后的原理:
- Lambda 内联:调用内联函数时,编译器将函数调用替换为其函数体,并将 lambda 直接插入调用上下文。如果调用上下文在协程内部,lambda 就继承了该协程上下文,从而能执行挂起调用。
- 挂起 Lambda :Kotlin 支持用
suspend修饰的挂起 lambda。当它们与内联函数结合使用时,编译器会生成协程感知的代码。 - 协程上下文:外围协程确保内联 lambda 中的挂起函数正确执行,而不会阻塞线程。
编译器处理上述 repeat() 的实际效果相当于:
kotlin
suspend fun main() {
for (i in 0 until 3) {
printMessage("skydoves $i")
}
}
内联过程把 repeat() 替换成了等价的循环,挂起函数 printMessage 得以直接执行。
总而言之。
repeat()、map()、filter() 之所以能接受挂起 lambda,是因为它们都是 inline 函数。内联让编译器能把函数调用替换为函数体,把挂起 lambda 无缝融入协程上下文。这充分体现了 Kotlin 内联函数与协程系统的强大与灵活性。