内联函数

136 阅读7分钟

内联函数,顾名思义,就是在编译时将作为函数参数的函数体直接映射到函数调用处,直接用一个例子来说明:

fun requestInfo() {
    getStr()
}

fun getStr() {
    println("inline")
}

很简单,getStr()中打印了一个字符串,然后requestInfo()中调用了getStr()函数,将上述代码转换成java代码之后:

public final void requestInfo() {
   this.getStr();
}

public final void getStr() {
   String var1 = "inline";
   System.out.println(var1);
}

继续,在getStr()的前面加上inline声明,如下:

fun requestInfo() {
    getStr()
}

//普通函数中并不推荐加inline关键字
inline fun getStr() {
    println("inline")
}

转换成java之后:

public final void requestInfo() {
   String var3 = "inline";
   System.out.println(var3);
}

可以看到转换成java之后的代码有明显的区别:加上inline之后,getStr()中的函数内容直接“复制粘贴”到requestInfo()中,即内联到函数调用处了

inline

通过上面的例子,inline的作用就很明显了,就是在编译时直接将函数内容直接复制粘贴到调用处。

我们知道函数调用最终是通过JVM操作数栈的栈帧完成的,每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程,使用了inline关键字理论上可以减少一个栈帧层级。

那么是不是所有的函数前面都适合加上inline关键字了呢?答案是否定的,其实JVM本身在编译时,就支持函数内联,并不是kotlin中特有的,那么kotlin中什么样的函数才需要使用inline关键字呢?答:高阶函数!

只有高阶函数中才需要inline去做内联优化,普通函数并不需要,如果在普通函数强行加上inline,编辑器会立刻提醒:

Expected performance impact from inlining is insignificant. Inlining works best for functions with parameters of functional types

意思是 内联对普通函数性能优化的预期影响是微不足道的。内联最适合带有函数类型参数的函数。

为什么高阶函数要使用inline

inline优化了什么问题呢?因为我们使用的Lambda表示式在编译转换后被换成了匿名类的实现方式。

fun requestInfo() {
    highFuc("inline") { str ->
        println(str)
    }
}

fun highFuc(name: String, block: (String) -> Unit) {
    block(name)
}

转换成java之后:

public final void requestInfo() {
   this.highFuc("inline", (Function1)null.INSTANCE);
}

private final void highFuc(String name, Function1 block) {
   block.invoke(name);
}

public interface Function1<in P1, out R> : Function<R> {
    public operator fun invoke(p1: P1): R
}

所以函数参数最终会转换成interface,并通过创建一个匿名实例来实现。这样就会造成额外的内存开销。为了解决这个问题,kotlin引入inline内联功能,将Lambda表达式带来的性能开销消除。还是上面的例子,这次我们对高阶函数添加inline关键字:

fun requestInfo() {
    highFuc("inline") { str ->
        println(str)
    }
}

//注意:这里添加了inline关键字
inline fun highFuc(name: String, block: (String) -> Unit) {
    block(name)
}

转换成java之后:

public final void requestInfo() {
   String name$iv = "inline";
   System.out.println(name$iv);
}

这里插个题外话:operator 关键字

451b07b55.png

当满足以下条件时:

  • 必须用 operator 修饰方法

  • 方法名称必须是 invoke

  • invoke 参数可以多个,不做限制

image.png

可以发现:

  • Test() 是创建了test对象。
  • test() 是调用了 Test类中的 invoke()方法,即 test() == test.invoke()。

reified

reified:使抽象的东西更加具体或真实,非常推荐 Android 开发使用这个关键字。本文介绍 3 点特别的使用方式如下:

1. 不再需要传参数 clazz

大部分的文章讲解 reified 的使用,都有提到这个点,比如我们定义实现一个扩展函数启动 Activity,一般都需要传 Class<T> 参数:

// Function
private fun <T : Activity> Activity.startActivity(context: Context, clazz: Class<T>) {
    startActivity(Intent(context, clazz))
}

// Caller
startActivity(context, NewActivity::class.java)

使用 reified,通过添加类型传递简化泛型参数

// Function
inline fun <reified T : Activity> Activity.startActivity(context: Context) {
    startActivity(Intent(context, T::class.java))
}

// Caller
startActivity<NewActivity>(context)

2. 不安全的转换

Kotlin 中, 使用安全转换操作符 as?,它可以在失败时返回 null。实现如下函数,我们认为会安全地获取数据或返回 null

// Function
fun <T> Bundle.getDataOrNull(): T? {
    return getSerializable(DATA_KEY) as? T
}

// Caller
val bundle: Bundle? = Bundle()
bundle?.putSerializable(DATA_KEY, "Testing")
val strData: String? = bundle?.getDataOrNull()
val intData: Int? = bundle?.getDataOrNull() // Crash

然而,如果获得的数据不是它期望的类型,这个函数会出现 crash。 因此为了安全获取数据,修改之前的函数如下:

// Function
fun <T> Bundle.getDataOrNull(clazz: Class<T>): T? {
    val data = getSerializable(DATA_KEY)
    return if (clazz.isInstance(data)) {
        data as T
    } else {
        null
    }
}

// Caller
val bundle: Bundle? = Bundle()
bundle?.putSerializable(DATA_KEY, "Testing")
val strData: String? = bundle?.getDataOrNull(String::class.java)
val intData: Int? = bundle?.getDataOrNull(String::class.java) // Null

这种写法不太友好,不仅在实现函数的方式上,而且还需要传递额外的 clazz 参数。

使用 reified,简化泛型参数和保证 as? 类型转换安全性

// Function
private inline fun <reified T> Bundle.getDataOrNull(): T? {
    return getSerializable(DATA_KEY) as? T
}

// Caller
val bundle: Bundle? = Bundle()
bundle?.putSerializable(DATA_KEY, "Testing")
val strData: String? = bundle?.getDataOrNull()
val intData: Int? = bundle?.getDataOrNull() // Null

3. 不同的返回类型函数重载

实现一个函数计算 DP 到像素,并返回一个 Int 或 Float。这种情况就会想到函数重载,如下所示:

fun Resources.dpToPx(value: Int): Float {
    return TypedValue.applyDimension(
        TypedValue.COMPLEX_UNIT_DIP,
        value.toFloat(), displayMetrics)
}

fun Resources.dpToPx(value: Int): Int {
    val floatValue: Float = dpToPx(value)
    return floatValue.toInt()
}

但是,这将导致编译时出错。原因是:函数重载方式只能根据参数计数和类型不同,而不能根据返回类型。

使用 reified,可以实现不同的返回类型函数重载

inline fun <reified T> Resources.dpToPx(value: Int): T {
    val result = TypedValue.applyDimension(
        TypedValue.COMPLEX_UNIT_DIP,
        value.toFloat(), displayMetrics)

    return when (T::class) {
        Float::class -> result as T
        Int::class -> result.toInt() as T
        else -> throw IllegalStateException("Type not supported")
    }
}

// Caller
val intValue: Int = resource.dpToPx(64)
val floatValue: Float = resource.dpToPx(64)

noinline

当函数被inline标记时,使用noinline可以使函数参数不被内联。

fun requestInfo() {
    highFuc({
        println("noinline")
    }, {
        println("inline")
    })
}

//highFuc被inline修饰,而函数参数block0()使用了noinline修饰
inline fun highFuc(noinline block0: () -> Unit, block1: () -> Unit) {
    block0()
    block1()
}

转换成java之后:

public final void requestInfo() {
   Function0 block0$iv = (Function0)null.INSTANCE;
   block0$iv.invoke();
   
   String var5 = "inline";
   System.out.println(var5);
}

结果也很明显,block0()函数没有被内联,而block()函数被内联,这就是noinline的作用。

如果想在非内联函数Lambda中直接return怎么办?比如我想这么写:

fun requestInfo() {
    highFuc {
        return //错误,不允许在非内联函数中直接return
    }
}

fun highFuc(block: () -> Unit) {
    println("before")
    block()
    println("after")
}

对不起,不允许!会直接在return的地方报**'return' is not allowed here**错误。 但是可以写成return@highFuc,即:

fun requestInfo() {
    highFuc {
        return@highFuc //正确,局部返回
    }
}

fun highFuc(block: () -> Unit) {
    println("before")
    block()
    println("after")
}

此时输出结果:

before
after

其中return是全局返回,会影响Lambda之后的执行流程;而return@highFuc是局部返回,不会影响Lambda之后的执行流程。如果我就想全局返回,那么可以通过inline来进行声明:

fun requestInfo() {
    highFuc {
        return 
    }
}

inline fun highFuc(block: () -> Unit) {
    println("before")
    block()
    println("after")
}

因为highFuc通过inline声明为内联函数,所以调用方可以直接使用return进行全局返回,执行requestInfo()的结果:

before

可以看到Lambda之后的after并没有被执行,因为是全局返回,当然可以改成return@highFuc局部返回,这样就可以都执行了。

如果此时还是想局部返回,使用 return@highFuc:

fun requestInfo() {
    highFuc {
         return@highFuc 
    }
}

inline fun highFuc(block: () -> Unit) {
    println("before")
    block()
    println("after")
}

此时输出结果:

before
after

结论:内联函数所引用的Lambda表达式中是可以使用return关键字来进行函数返回的,而非内联函数只能进行局部返回

现在有一种场景,我既想使用inline优化高阶函数,同时又不想调用方打断我的执行流程(因为inline是支持全局return的),貌似冲突了,这时候怎么办呢,这时候就需要crossinline了。

crossinline

允许inline内联函数里的函数类型参数可以被间接调用,但是不能在Lambda表达式中使用全局return返回。

fun requestInfo() {
    highFuc {
        return //错误,虽然是inline内联函数,但Lambda中使用crossinline修饰,所以不允许全局返回了
    }
}

inline fun highFuc(crossinline block: () -> Unit) {
    println("before")
    block()
    println("after")
}

crossinline关键字就像一个契约,它用于保证内联函数的Lambda表达式中一定不会使用return全局返回,这样就不会冲突了。当然return@highFuc局部返回还是可以的。

总结

  • inline:编译时直接将函数内容直接复制粘贴到调用处。
  • noinline:当函数被inline标记时,使用noinline可以使函数参数不被内联。
  • crossinline: 允许内联函数里的函数类型参数可以被间接调用,但是不能在Lambda表达式中使用全局return返回。