一、内联函数家族
1) inline ------ 代码"展开到调用点"
是什么 :编译器把函数体(以及可内联的 lambda)直接替换到调用处,避免创建 FunctionN
对象和虚调用开销。
好处 :减少分配与调度;允许 reified
(具体化泛型)。
风险:代码体大/调用多 → 字节码膨胀;堆栈回溯不如非内联直观。
范式
kotlin
kotlin
复制编辑
inline fun <T> measure(tag: String, block: () -> T): T {
val t0 = System.nanoTime()
return try { block() } finally {
println("$tag took ${(System.nanoTime()-t0)/1e6}ms")
}
}
// 调用点不会分配 Function 对象
val data = measure("load") { repo.load() }
2) noinline ------ 这个 lambda 不要内联
什么时候用 :当某个参数 lambda 需要被保存/传递(赋给字段、放集合、延迟执行)时,必须禁止内联。
kotlin
kotlin
复制编辑
inline fun process(doNow: () -> Unit, noinline doLater: () -> Unit) {
doNow() // 可内联
scheduler.post(doLater) // 需要当值传递 → noinline
}
带
noinline
的参数不能 用reified
能力(见下节)。
3) crossinline ------ 禁止"非局部返回"
背景 :内联 lambda 默认允许 非局部返回 (return
直接从外层函数返回)。
问题 :如果 lambda 在其他线程/延迟 执行,非局部返回会破坏控制流。
解决 :用 crossinline
禁止非局部返回,只能 return@label
局部返回。
kotlin
kotlin
复制编辑
inline fun onBg(crossinline block: () -> Unit) {
Thread { block() }.start() // 可能跨线程执行
}
fun foo() {
onBg {
// return // ❌ 编译错误(禁止非局部返回)
return@onBg // ✅ 局部返回
}
}
4) reified ------ 具体化泛型(只在 inline 中可用)
动机 :JVM 泛型类型擦除导致运行时拿不到 T
的类型信息。
做法 :在 inline
中用 reified T
,编译时把"实际类型"写死到调用点,允许 is T
、T::class
。
kotlin
kotlin
复制编辑
inline fun <reified T> Gson.fromJson(json: String): T =
fromJson(json, T::class.java)
val user: User = Gson().fromJson(json) // 省去 TypeToken
限制 :只能在 inline
函数的 reified 类型参数 上使用;Java 端无法直接调用这种 API 享受 reified,好在可暴露重载。
5) 搭配与边界
inline
+reified
:类型检查/反射/序列化最常用组合(filterIsInstance<T>()
、startActivity<T>()
)。noinline
参数不能reified
;需要两种语义时可并存:inline fun f(a: ()->Unit, noinline b: ()->Unit)
。crossinline
常见于回调/线程/协程 边界,禁止return
逃逸外层。- 过度内联会膨胀字节码;复杂长函数慎用。
一、基础理解类
Q1. 什么是内联函数(inline function)?为什么需要?
答法:
-
定义 :编译器会把函数体 直接展开 到调用处,避免生成额外的函数调用栈和 lambda 对象。
-
目的:
- 性能优化:减少函数调用开销和 lambda 对象分配(减少 GC 压力)。
- 突破限制 :允许 lambda 内的非局部返回(
return
直接返回外层函数)。
-
适用场景:
- 高阶函数(参数是 lambda)且调用频繁。
- 避免在热路径频繁分配闭包对象。
深挖点:
- 内联是编译期优化,跟 JVM 的 JIT inline 不冲突(Kotlin 编译器做一次,JVM JIT 还会做一次)。
- 如果函数很大,内联可能导致 字节码膨胀(code bloat),所以不建议滥用。
Q2. 内联函数与普通函数在字节码上的区别?
答法:
-
普通函数:生成独立的方法字节码 + lambda 会编译成
FunctionN
实现类。 -
内联函数:函数体和 lambda 复制展开到调用点,lambda 内部变量捕获直接用外层变量,无闭包类。
-
好处:
- 减少对象创建(闭包类、捕获对象)。
- 减少一次栈帧调用。
-
坏处:
- 调用点多 → 字节码重复多次 → 增加 APK 体积。
二、进阶应用类
Q3. 为什么 inline 能支持 lambda 的非局部返回(non-local return)?
答法:
- 因为 lambda 被内联展开到调用点,相当于 lambda 代码直接放在外层函数中执行,所以可以
return
到外层函数。 - 普通高阶函数传 lambda 是运行时回调,lambda 只能返回自身,不可能跳出外层。
示例:
kotlin
kotlin
复制编辑
inline fun forEach(list: List<Int>, action: (Int) -> Unit) {
for (i in list) action(i)
}
fun test() {
forEach(listOf(1,2,3)) {
if (it == 2) return // 非局部返回(直接跳出 test)
}
}
Q4. noinline 的作用是什么?
答法:
-
noinline
表示这个 lambda 不内联 ,即使所在函数是inline
。 -
使用场景:
- 需要把 lambda 存储到变量 或 作为返回值(内联 lambda 不能存储)。
- 需要把 lambda 传递到其他地方延迟执行。
示例:
kotlin
kotlin
复制编辑
inline fun test(inlined: () -> Unit, noinline nonInlined: () -> Unit) {
inlined() // 内联
val f = nonInlined // 可以存储
}
Q5. crossinline 的作用是什么?
答法:
crossinline
表示 lambda 必须内联 ,但禁止非局部返回。- 场景:内联 lambda 会被放到另一个函数/线程中执行(异步执行),如果允许非局部返回会破坏调用逻辑。
- 解决 :
crossinline
让编译器禁止使用return
返回外层函数。
示例:
kotlin
kotlin
复制编辑
inline fun runAsync(crossinline block: () -> Unit) {
Thread { block() }.start()
}
Q6. reified 为什么必须和 inline 一起用?
答法:
- 原因 :JVM 泛型类型擦除,运行时拿不到
T
。 inline
把函数展开到调用点,编译器可以把实际类型写进字节码,因此T
在运行时可用。- 没有
inline
的普通泛型函数,编译后T
消失,不能is T
或T::class
。
示例:
kotlin
kotlin
复制编辑
inline fun <reified T> Gson.fromJson(json: String): T =
fromJson(json, T::class.java)
三、原理追问类
Q7. inline 会影响调试和异常堆栈吗?
答法:
- 会的。因为代码被展开到调用点,异常堆栈的函数名可能丢失(编译器会生成
@InlineOnly
标记和合成方法)。 - 调试断点可能不进入"原始函数",而是直接在调用点执行。
Q8. inline 会影响递归函数吗?
答法:
-
不能直接递归,因为内联会无限展开编译失败。
-
解决:
- 取消内联;
- 或把递归改为迭代。
Q9. inline 滥用有什么问题?
答法:
- 代码体积膨胀(调用点多 → 复制多)。
- 反编译堆栈可读性下降。
- 大函数/调用少时,收益小甚至负收益。
- 复杂捕获变量场景,反而增加编译复杂度。
四、常见陷阱类
Q10. noinline 的 lambda 能 reified 吗?
答法:
- 不能。
reified
必须依赖 inline 展开到调用点,noinline
lambda 是运行时传递,类型擦除后拿不到具体类型。
Q11. inline 与 Java 互操作的注意事项?
答法:
- Java 调用 Kotlin
inline
函数,看到的就是一个普通方法(因为内联只在 Kotlin 编译期起作用)。 - 如果 Java 调用
reified
泛型,会报错(Java 无法传递类型参数给 reified),需要提供一个非 reified 重载。
五. 性能与使用场景
Q12. inline 适合用在哪些场景?
- 小而频繁调用的高阶函数(
forEach
、map
、UI 回调)。 - 工具方法(时间统计、日志埋点)。
- 需要 reified 泛型的场景(类型检查、反射、序列化解析)。
Q13. inline 不适合用在哪些场景?
- 函数体过大(会导致字节码爆炸)。
- 调用次数少的工具方法(没有性能收益,还增加体积)。
- 递归函数(会无限展开)。
六、面试速记口诀
内联为快,noinline 能存,crossinline 防 return,reified 破擦除
用在高频 lambda 调用,不要滥用大函数;Java 调用 reified 要加普通重载。