一、内联函数核心概念
1. 什么是内联函数?
内联函数通过 inline 关键字修饰,其核心思想是:在编译时将函数体直接插入到调用处,而非进行传统的函数调用。这意味着:
- 消除了函数调用的栈帧创建、参数传递等开销。
- 对 Lambda 表达式进行深度优化,避免匿名类对象的创建。
2. 与高阶函数的关系
高阶函数是将函数作为参数或返回值的函数(如 map、filter),而内联函数常作为高阶函数的 "优化搭档"。当高阶函数接收 Lambda 表达式时,配合 inline 关键字可显著提升性能。
大厂真题示例
问 :为什么 Kotlin 的
let函数要声明为内联函数?不内联会有什么问题?
答 :
let是接收 Lambda 参数的高阶函数,若不声明inline,Lambda 会被编译为匿名类对象(每次调用创建新实例),增加内存开销。声明
inline后,Lambda 代码直接嵌入调用处,避免对象创建,同时消除函数调用栈开销,提升高频调用时的性能(例如在集合遍历或 UI 链式配置中)。
二、常用内联函数源码剖析
1. let 函数:对象操作的灵活助手
        
            
            
              Kotlin
              
              
            
          
          @kotlin.internal.InlineOnly
public inline fun <T, R> T.let(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block(this)
}- 源码解读 :let函数是一个扩展函数,接收一个 Lambda 表达式block作为参数。@kotlin.internal.InlineOnly注解表明该函数只能内联调用。contract部分告诉编译器blockLambda 表达式只会被调用一次。return block(this)将调用对象this作为参数传递给block并返回其结果。
- 创新应用场景 :在处理可空对象时,let函数可以安全地对对象进行操作。例如,我们可以结合let函数和run函数实现更复杂的链式操作
2. run 函数:代码块执行的得力干将
        
            
            
              Kotlin
              
              
            
          
          @kotlin.internal.InlineOnly
public inline fun <T, R> T.run(block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}- 源码解读 :同样是扩展函数,run函数接收的 Lambda 表达式block以调用对象this作为接收者。通过contract告知编译器block只会被调用一次,最后执行block并返回结果。
- 创新应用场景 :在进行对象初始化和配置时,run函数可以让代码更加简洁。我们可以结合apply函数,实现对象的初始化和后续操作的链式调
3. with 函数:对象上下文的贴心陪伴
        
            
            
              Kotlin
              
              
            
          
          @kotlin.internal.InlineOnly
public inline fun <T, R> with(receiver: T, block: T.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return receiver.block()
}- 源码解读 :with函数是普通函数,接收一个对象receiver和一个 Lambda 表达式block。contract保证block只被调用一次,通过receiver.block()以receiver为接收者执行block并返回结果。
- 创新应用场景 :在处理集合时,with函数可以方便地对集合进行操作。我们可以结合also函数,在对集合进行操作的同时记录日志。
            
            
              Kotlin
              
              
            
          
          val numbers = listOf(1, 2, 3, 4, 5)
val sum = with(numbers) {
    filter { it % 2 == 0 }.sum()
}.also {
    println("The sum of even numbers is: $it")
}4. apply 函数:对象配置的链式大师
        
            
            
              Kotlin
              
              
            
          
          @kotlin.internal.InlineOnly
public inline fun <T> T.apply(block: T.() -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block()
    return this
}- 源码解读 :apply函数接收一个无返回值的 Lambda 表达式block,以调用对象this为接收者执行block,最后返回调用对象本身,支持链式调用。
- 创新应用场景 :在创建复杂对象时,apply函数可以让对象的配置更加清晰。我们可以结合let函数,在对象配置完成后进行一些额外的处理。
            
            
              Kotlin
              
              
            
          
          val button = Button(context).apply {
    text = "Click me"
    setOnClickListener { /* ... */ }
}.let {
    it.layoutParams = LinearLayout.LayoutParams(
        LinearLayout.LayoutParams.WRAP_CONTENT,
        LinearLayout.LayoutParams.WRAP_CONTENT
    )
    it
}5. also 函数:对象副作用的处理专家
        
            
            
              Kotlin
              
              
            
          
          @kotlin.internal.InlineOnly
public inline fun <T> T.also(block: (T) -> Unit): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    block(this)
    return this
}- 源码解读 :also函数接收一个 Lambda 表达式block,将调用对象this作为参数传递给block,执行block后返回调用对象本身。
- 创新应用场景 :在对对象进行操作时,also函数可以方便地进行副作用处理。我们可以结合run函数,在对象操作前后进行不同的处理。
            
            
              Kotlin
              
              
            
          
          val file = File("example.txt").also {
    it.createNewFile()
}.run {
    writeText("Hello, World!")
    this
}5 大函数对比表
| 函数 | 作用 | 接收者 / 参数 | 返回值 | Lambda 内 this指向 | 典型场景 | 
|---|---|---|---|---|---|
| let | 安全调用 + 作用域内变量重命名 | 扩展函数( T.let) | Lambda 返回值 | 函数参数( it) | 可空对象判空后操作: str?.let { ... } | 
| run | 对象作用域内执行代码块 | 扩展函数( T.run) | Lambda 返回值 | 当前对象( this) | 对象配置后需要返回特定结果: view.run { init(); calculate() } | 
| apply | 对象配置(链式调用) | 扩展函数( T.apply) | 当前对象( this) | 当前对象( this) | 对象初始化: Button().apply { text="OK"; onClick={...} } | 
| also | 副作用处理(记录日志 / 调试) | 扩展函数( T.also) | 当前对象( this) | 函数参数( it) | 操作后返回原对象: file.also { log(it) }.delete() | 
| with | 进入对象上下文(非扩展函数) | 普通函数( with(receiver, block)) | Lambda 返回值 | 接收者对象( this) | 避免重复书写对象名: with(list) { sort(); filter(...); } | 
大厂真题示例
问 :
apply和also有什么区别?请用代码举例说明。
答:
this指向不同 :
apply中this是当前对象,可直接调用成员(如text = "OK");
also中this是函数参数,需用it访问对象(如it.text = "OK")。- 返回值不同 :
apply返回当前对象(用于链式配置);
also也返回当前对象,但更侧重执行副作用(如日志、校验)。
示例:
java// apply:对象配置,直接使用this val button = Button(context).apply { text = "Submit" // 直接访问成员 setOnClickListener { ... } } // also:副作用处理,用it访问对象 button.also { log("Button created: ${it.id}") // 记录日志 }.setLayoutParams(...) // 链式调用
3 大核心原理(需结合编译过程说明)
- 
消除函数调用栈: - 传统函数调用:压栈→参数传递→执行→弹栈(有固定开销)。
- 内联后:函数体直接替换调用处,如 add(1,2)编译后变为1+2,无栈操作。
 
- 
Lambda 去对象化: - 非内联高阶函数:Lambda 编译为 Function接口的匿名类(如(T) -> R对应java.util.function.Function),每次调用创建新对象。
- 内联后:Lambda 代码直接嵌入,无对象创建(尤其适合高频调用的场景,如循环内的集合操作)。
 
- 非内联高阶函数:Lambda 编译为 
- 
编译器深度优化: - 内联后的代码可进行常量折叠 (如 inline fun a() = 1+1调用处直接替换为2)、死代码消除(如条件不成立的分支直接删除)。
 
- 内联后的代码可进行常量折叠 (如 
大厂真题示例
问 :内联函数一定比普通函数快吗?为什么?
答 :不一定,需结合场景:
- 优势场景:短函数 + 高频调用(如标准库工具函数),或含 Lambda 的高阶函数(避免对象创建)。
- 劣势场景 :长函数内联会导致代码膨胀 (函数体复制到所有调用处,增加 APK 体积);若函数仅调用一次,内联的开销(编译时间)可能超过运行时收益。
最佳实践 :只对小而频繁调用的高阶函数使用内联(如 Kotlin 标准库的设计原则)。
三、高频陷阱:内联函数的注意事项
1. 非局部返回风险
- 问题 :内联函数中的 Lambda 可通过 return直接跳出外层函数,导致逻辑混乱(如在协程或循环中误用)。
- 解决方案 :
- 
用 crossinline修饰 Lambda,禁止非局部返回(只能用return@label局部返回)。
- 
面试示例 : Kotlininline fun withAction(block: () -> Unit) { println("Before") block() // 若block中用return,会直接跳出外层函数 println("After") // 可能不执行 } // 修正:用crossinline避免意外返回 inline fun withAction(crossinline block: () -> Unit) { ... }
 
- 
2. 泛型类型擦除限制
- 
问题 :内联函数无法获取泛型的实际类型(如 inline fun <T> f(t: T)中,运行时T被擦除)。
- 
解决 :配合 reified关键字保留类型信息(需结合inline使用):Kotlininline fun <reified T> checkType(obj: Any) = obj is T // 使用:checkType<String>("abc") // 编译时知道T是String
3. 代码膨胀与性能平衡
- 现象:内联函数被调用 100 次,函数体代码复制 100 次,可能导致 APK 体积增大。
- 面试回答 :
 "应遵循'小而频'原则:仅对代码量小(如几行)、调用频繁(如循环内)的函数内联。对于长函数,即使含 Lambda,也可能因代码膨胀导致性能下降,此时需权衡内存(Lambda 对象)与体积(代码复制)的取舍。"
四、面试加分项:内联函数在架构组件中的应用
1. LiveData 的 observe 方法为何不内联?
- 答 :observe接收的Observer是接口而非 Lambda,无需内联(接口实现本身是对象,内联无法优化)。
- 延伸 :Kotlin 的 liveData构建器中使用内联函数优化协程上下文,避免匿名类创建。
2. ViewModel 的工厂函数是否需要内联?
- 答 :不需要。ViewModel 工厂(如 ViewModelProvider.Factory)是接口,其create方法的实现是类而非 Lambda,内联无优化作用。内联主要针对 Lambda 参数的高阶函数。
还有扩展链接如下:
 https://blog.csdn.net/2301_80329517/article/details/146914048?spm=1011.2415.3001.5331
https://blog.csdn.net/2301_80329517/article/details/146914048?spm=1011.2415.3001.5331