内联函数 inline noinline crossinline reified

一、内联函数家族

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 TT::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 对象。

  • 目的

    1. 性能优化:减少函数调用开销和 lambda 对象分配(减少 GC 压力)。
    2. 突破限制 :允许 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

  • 使用场景:

    1. 需要把 lambda 存储到变量作为返回值(内联 lambda 不能存储)。
    2. 需要把 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 TT::class

示例

kotlin 复制代码
kotlin
复制编辑
inline fun <reified T> Gson.fromJson(json: String): T =
    fromJson(json, T::class.java)

三、原理追问类

Q7. inline 会影响调试和异常堆栈吗?

答法:

  • 会的。因为代码被展开到调用点,异常堆栈的函数名可能丢失(编译器会生成 @InlineOnly 标记和合成方法)。
  • 调试断点可能不进入"原始函数",而是直接在调用点执行。

Q8. inline 会影响递归函数吗?

答法:

  • 不能直接递归,因为内联会无限展开编译失败。

  • 解决:

    • 取消内联;
    • 或把递归改为迭代。

Q9. inline 滥用有什么问题?

答法:

  1. 代码体积膨胀(调用点多 → 复制多)。
  2. 反编译堆栈可读性下降。
  3. 大函数/调用少时,收益小甚至负收益。
  4. 复杂捕获变量场景,反而增加编译复杂度。

四、常见陷阱类

Q10. noinline 的 lambda 能 reified 吗?

答法:

  • 不能。reified 必须依赖 inline 展开到调用点,noinline lambda 是运行时传递,类型擦除后拿不到具体类型。

Q11. inline 与 Java 互操作的注意事项?

答法:

  • Java 调用 Kotlin inline 函数,看到的就是一个普通方法(因为内联只在 Kotlin 编译期起作用)。
  • 如果 Java 调用 reified 泛型,会报错(Java 无法传递类型参数给 reified),需要提供一个非 reified 重载。

五. 性能与使用场景

Q12. inline 适合用在哪些场景?

  • 小而频繁调用的高阶函数(forEachmap、UI 回调)。
  • 工具方法(时间统计、日志埋点)。
  • 需要 reified 泛型的场景(类型检查、反射、序列化解析)。

Q13. inline 不适合用在哪些场景?

  • 函数体过大(会导致字节码爆炸)。
  • 调用次数少的工具方法(没有性能收益,还增加体积)。
  • 递归函数(会无限展开)。

六、面试速记口诀

内联为快,noinline 能存,crossinline 防 return,reified 破擦除

用在高频 lambda 调用,不要滥用大函数;Java 调用 reified 要加普通重载。

相关推荐
踢球的打工仔3 小时前
PHP面向对象(7)
android·开发语言·php
安卓理事人3 小时前
安卓socket
android
安卓理事人9 小时前
安卓LinkedBlockingQueue消息队列
android
万能的小裴同学10 小时前
Android M3U8视频播放器
android·音视频
q***577411 小时前
MySql的慢查询(慢日志)
android·mysql·adb
JavaNoober11 小时前
Android 前台服务 "Bad Notification" 崩溃机制分析文档
android
城东米粉儿12 小时前
关于ObjectAnimator
android
zhangphil13 小时前
Android渲染线程Render Thread的RenderNode与DisplayList,引用Bitmap及Open GL纹理上传GPU
android
火柴就是我14 小时前
从头写一个自己的app
android·前端·flutter
lichong95115 小时前
XLog debug 开启打印日志,release 关闭打印日志
android·java·前端