深入 Kotlin 内联函数全链路解析:从 inline 字节码内联到 reified 泛型特化的编译期优化黑魔法

Kotlin 里每传一个 lambda,编译器都会生成匿名内部类对象。Compose 的 @Composable 函数在密集重组时性能不崩,核心原因就是编译器把函数体直接”搬”到了调用处——inline 的工作方式。

inline 远不止消除函数调用开销。它对泛型擦除、非局部返回、字节码体积的实际影响,才是日常开发里容易踩坑的地方。

inline 在字节码里做了什么

inline fun measureTime(block: () -> Unit) {
    val start = System.nanoTime()
    block()
    println("cost: ${System.nanoTime() - start}ns")
}

fun main() {
    measureTime { Thread.sleep(100) }
}

javap -c 反编译 main 方法,能看到 System.nanoTime() 调用和 println 逻辑直接嵌在 main 里,不存在对 measureTime 的方法调用。lambda 参数 block 被展开为 Thread.sleep(100) 表达式本身,匿名内部类对象的创建也被抹掉了。

编译期把函数体和 lambda 参数一起”粘贴”到调用点,运行时零开销。

代价同样直接——每个调用点都复制一份完整的函数体。内联函数逻辑膨胀且被高频调用时,dex 体积增长明显。我在一个大型项目里统计过高频调用的 inline 工具方法,去掉不必要的 inline 后 dex 缩减了约 3%。包体积敏感的场景里,这个收益值得纳入考量。

noinline:阻止不想内联的参数

inline 函数默认内联所有 lambda 参数。想把某个 lambda 传给另一个非内联函数——比如存起来做回调——就不能让它内联。内联后的代码不存在函数对象,根本没东西可传。

inline fun transaction(
    db: Database,
    noinline onComplete: () -> Unit,
    block: () -> Unit
) {
    db.beginTransaction()
    try {
        block()           // 内联展开
        db.setTransactionSuccessful()
    } finally {
        db.endTransaction()
        onComplete()      // 保留函数对象,可安全传递
    }
}

onComplete 加了 noinline 后,编译器保留它的匿名内部类实例,可以赋值给变量或作为参数传递。不加的话编译器直接报错——它知道你引用的东西内联后就不存在了。

实际项目里我的习惯是:核心高频逻辑用 inline 展开,副作用回调标记为 noinline。这是性能和控制力的平衡点。

crossinline:禁止非局部返回

三个修饰符里,crossinline 坑最多。先看问题本质:

inline fun runSafely(block: () -> Unit) {
    try {
        block()
    } finally {
        println("cleanup")
    }
}

fun main() {
    runSafely {
        return  // 结束的是 main 函数,不是 lambda
    }
}

因为 block 被内联进了 main 的函数体,return 直接结束 mainfinally 块依然执行。这是非局部返回(non-local return),Kotlin 里一个容易被忽视的设计。

block 被放入另一个执行上下文——比如启动协程或 post 到 Handler——非局部返回就炸了:

inline fun postTask(crossinline block: () -> Unit) {
    Handler(Looper.getMainLooper()).post {
        block()  // block 在另一个 lambda 里执行
    }
}

必须加 crossinline,告诉编译器:block 会被间接调用,禁止在 block 里用 return。编译器在调用处检查语法,发现 return 直接报错。

crossinline 里的”cross”,指的是 lambda 跨越了当前函数的直接调用边界,被传到另一个上下文里执行。这个语义和 @Composable 的重组上下文切换天然契合。

reified:用编译期类型替换打破擦除

JVM 泛型在运行时被擦除,T::class.java 这种写法编译不过。除非用 reified

inline fun <reified T> Gson.fromJson(json: String): T {
    return fromJson(json, T::class.java)  // 编译通过
}

reified 的原理不复杂:函数体内联后,编译器知道调用点的具体类型实参。调用 fromJson<User>(json) 时,编译期把所有 T 替换成 UserT::class.java 变成 User::class.java。字节码里没有泛型,只有具体类型。

配合 inline + reified,序列化、类型判断场景能省掉大量样板代码。Gson、kotlinx.serialization 的 API 都走这套机制。

Compose 里的实际战场

Compose 的 @Composable 函数几乎都带 inline。

场景一:rememberSaveable 的 reified 类型获取

@Composable
inline fun <reified T : Any> rememberSaveable(
    key: String,
    crossinline init: () -> T
): T {
    val saver = autoSaver<T>()  // 编译期推导 T 的具体 Saver
    return rememberSaveable(key, saver, init)
}

autoSaver<T>() 能自动生成正确的序列化器,完全依赖 reified 在编译期拿到 T 的真实类型。没有 reified 就只能手动传 Class<T>

场景二:crossinline 在 Composable lambda 里的约束

itemContent 被传给 LazyColumnitems lambda,调用上下文切换了。crossinline 不光是阻止 return,它还确保编译器生成正确的 Composer 插桩代码,让重组机制在正确的组合作用域内启动。

场景三:inline 与 @Composable 的底层配合

RowColumn 这类布局组件都是 inline 的,调用时不产生 lambda 对象分配。Compose 编译器插件处理 @Composable lambda 的内联展开,把它们变成带 $composer 参数的直连调用图。大量重组时没有对象分配压力,靠的就是编译期展开。

什么时候用,什么时候克制

写 inline 之前问自己三个问题:

  1. 这个函数会被高频调用吗? Compose 的 Composable 函数和集合操作类工具函数,inline 减少对象分配的收益是实打实的。调用频次到不了两位数的普通工具方法,省掉 inline 还能省 dex 体积。

  2. 参数是 lambda 吗? 没有 lambda 参数只有函数体内联收益,很微弱。IntelliJ 会给黄色警告,这份提醒值得听。

  3. 是否需要 reified 或非局部返回? 这两个是硬需求,不加 inline 编译不过。

踩过一个坑:给返回 Result 的 suspend 函数加了 inline,编译直接报错——inline 函数不能调用非内联的 suspend 函数,协程的状态机没法内联展开。这类边界情况在官方文档里交代得很清楚,但实际撞上之前很少会注意到。

inline 本质是一场编译期的置换游戏:用字节码体积换运行时性能,用编译期类型信息补 JVM 泛型擦除的短板。搞清楚它和 noinline、crossinline、reified 在字节码层面的真实行为,比背面试题有用得多。