内联函数 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 要加普通重载。

相关推荐
whysqwhw3 小时前
安卓上WebRtc
android
wayne2143 小时前
从 MVC 到 MVI:Android 架构演进全景剖析与示例代码
android
我又来搬代码了5 小时前
【Android】【bug】Json解析错误Expected BEGIN_OBJECT but was STRING...
android·json·bug
CANI_PLUS14 小时前
ESP32将DHT11温湿度传感器采集的数据上传到XAMPP的MySQL数据库
android·数据库·mysql
来来走走15 小时前
Flutter SharedPreferences存储数据基本使用
android·flutter
安卓开发者16 小时前
Android模块化架构深度解析:从设计到实践
android·架构
雨白17 小时前
HTTP协议详解(二):深入理解Header与Body
android·http
阿豪元代码17 小时前
深入理解 SurfaceFlinger —— 如何调试 SurfaceFlinger
android
阿豪元代码17 小时前
深入理解 SurfaceFlinger —— 概述
android