解析Kotlin中的内联函数,inline、noinline、crossinline【笔记摘要】

用编译时常量的概念,引出本文要讲内联函数inline:

1.编译时常量

  • Java的编译时常量 Compile-time Constant

它有四个要求:1.这个变量需要是 final 的  2.类型只能是字符串或者基本类型  3.这个变量需要在声明的时候就赋值  4.等号右边还不能太复杂

final String name = "hsf";
final int age = 18;
final long current = System.currentTimeMillis();

这种编译时常量,会被编译器以内联的形式进行编译,也就是直接把你的值拿过去替换掉调用处的变量名来编译。这样一来,程序结构就变简单了,编译器和 JVM 也方便做各种优化。这,就是编译时常量的作用。

  • 这种编译时常量,到了 Kotlin 里有了一个专有的关键字,叫 const

一个变量如果以 const val 开头,它就会被编译器当做编译时常量来进行内联式编译:

在这里插入图片描述

当然你得符合编译时常量的特征啊,不然会报错,不给编。
在这里插入图片描述

让变量内联用的是 const;而除了变量,Kotlin 还增加了对函数进行内联的支持。在 Kotlin 里,你给一个函数加上 inline 关键字,这个函数就会被以内联的方式进行编译。
但!虽然同为内联,inline 关键字的作用和目的跟 const 是完全不同

2.inline出现的原因

事实上,inline 关键字不止可以内联自己的内部代码,还可以内联自己内部的内部的代码。什么叫「内部的内部」?就是自己的函数类型的参数

声明一个函数,其有一个函数类型的参数
在这里插入图片描述

我可以填成匿名函数的形式:
在这里插入图片描述

也可以简单点,写成 Lambda 表达式:
在这里插入图片描述

因为 Java 并没有对函数类型的变量的原生支持,Kotlin 需要想办法来让这种自己新引入的概念在 JVM 中落地,就是用一个 JVM 对象来作为函数类型的变量的实际载体,让这个对象去执行实际的代码。

也就是说,在我对代码做了刚才那种修改之后,程序在每次调用 hello() 的时候都会创建一个对象来执行 Lambda 表达式里的代码,虽然这个对象是用一下之后马上就被抛弃,但它确实被创建了。
在这里插入图片描述

但是如果这种函数被放在循环里执行,内存占用一下就飚起来了。而且关键是,你作为函数的创建者,并不知道、也没法规定别人在什么地方调用这个函数,也就是说,这个函数是否出现在循环或者界面刷新之类的高频场景里,是完全不可控的。

3.inline的作用

函数在被加了 inline 关键字之后,编译器在编译时不仅会把函数内联过来,而且会把它内部的函数类型的参数也内联过来。换句话说,这个函数被编译器贴过来的时候是完全展开铺平的:
在这里插入图片描述

经过这种优化,就避免了函数类型的参数所造成的临时对象的创建。这样的话,就不怕在循环或者界面刷新这样的高频场景里调用它们了
在这里插入图片描述

这就是 inline 关键字的用处:高阶函数(Higher-order Functions)有它们天然的性能缺陷,我们通过 inline 关键字让函数用内联的方式进行编译,来减少参数对象的创建,从而避免出现性能问题

4.noinline

当一个函数被内联之后,它内部的那些函数类型的参数就不再是对象了,因为它们的壳被脱掉了。换句话说,对于编译之后的字节码来说,这个对象根本就不存在。一个不存在的对象,你怎么使用?
所以当你要把一个这样的参数当做对象使用的时候,Android Studio 会报错,告诉你这没法编译:
在这里插入图片描述

那……我如果真的需要用这个对象怎么办?加上 noinline:
在这里插入图片描述

加了 noinline 之后,这个参数就不会参与内联了:
在这里插入图片描述

noinline 的作用:用来局部地、指向性地关掉函数的内联优化的。使得函数中的函数类型的参数可能被当做对象来使用

5.在Lamdba中使用return

//情况一
fun hello(postAction: () -> Unit) {
    LogUtil.d("Hello!")
    postAction()
}
//情况二
inline fun hello(postAction: () -> Unit) {
    LogUtil.d("Hello!")
    postAction()
}

hello {
    LogUtil.d("Bye!")
    return  //实际上对于情况一,这里编译过不了
    LogUtil.d("Bye!2")
}

对于函数参数中的Lambda 的 return,我们有这样的直观感受。如果是在非内联函数中,return的应该是hello;如果是在内联函数中,return的应该是hello的外层函数。这就造成了一种歧义,那我一个 return 结束哪个函数,竟然要看这个函数是不是内联函数!那岂不是我每次写这种代码都得钻到原函数里去看看有没有 inline 关键字,才能知道我的代码会怎么执行?那这也太难了吧!

为了消除在Lamdba中return所带来的歧义,Kotlin指定了Lambda中return的规则:

  • 规则1、只有内联函数的 Lambda 参数可以使用 return。
fun hello(postAction: () -> Unit) {
    LogUtil.d("Hello!")
    postAction()
}

hello {
    LogUtil.d("Bye!")
    return  //编译不通过,提示return' is not allowed here
    LogUtil.d("Bye!2")
}

注意:如果给函数参数又加上了noinline,那么lambda中的return又报错了,很简单,因为它不属于内联的参数了,它又不是铺平的了,此时它的return又变得有歧义了

  • 规则2、Lambda 里的 return,结束的不是直接的外层函数,而是外层再外层的函数(因为内联函数已经被铺平了)
inline fun hello(postAction: () -> Unit) {
    LogUtil.d("Hello!")
    postAction()
}

hello {
    LogUtil.d("Bye!")
    return  //编译通过,这个return结束的是hello外层的函数
    LogUtil.d("Bye!2")
}

注意:非Lamdba,也就是那种用fun来写的函数类型参数,在是否内联函数中都可以使用return,因为它都结束的是自己的这个fun

fun hello(postAction: () -> Unit) {
    LogUtil.d("Hello!")
    postAction()
}

hello(fun() {
    LogUtil.d("Bye!")
    return  //这个return结束的是hello
    LogUtil.d("Bye!2")
})

6.crossinline

如果我要对内联函数里的函数类型的参数进行间接调用,例如:

fun ppp(runnable: Runnable) {
	...
}

inline fun hello(postAction: () -> Unit) {
    LogUtil.d("Hello!")
    ppp{
        //实际这里编译不通过,提示: Can't inline 'postAction' here: it may contain non-local returns. Add 'crossinline' modifier to parameter declaration 'postAction'
        postAction()  
    }
}

fun main(){
    hello {
        LogUtil.d("Bye!")
        return
        LogUtil.d("Bye!2")
    }
}

这就带来了一个麻烦:本来在调用处行的 return 是要结束它外层再外层的 main() 函数的,但现在因为它被放在了 ppp() 里,hello() 对它的调用就变成了间接调用。所谓间接调用,直白点说就是它和外层的 hello() 函数之间的关系被切断了。和 hello() 的关系被切断,那就更够不着更外层的 main() 了,也就是说这个间接调用,导致 Lambda 里的 return 无法结束最外面的 main() 函数了。
因此Kotlin选择了,干脆内联函数里的函数类型的参数,不允许这种间接调用

那我如果真的有这种需求呢?如果我真的需要间接调用,怎么办?使用 crossinline。crossinline 也是一个用在参数上的关键字。当你给一个需要被间接调用的参数加上 crossinline,就对它解除了这个限制,从而就可以对它进行间接调用了:

inline fun hello(crossinline postAction: () -> Unit) {
    LogUtil.d("Hello!")
    ppp{
        postAction()
    }
}

不过这就又会导致前面说过的return歧义的问题,它结束的是谁?是包着它的 ppp(),还是依然是hello的外层?

hello {
    LogUtil.d("Bye!")
    return
    LogUtil.d("Bye!2")
}

对于这种不一致,Kotlin 增加了一条额外规定:内联函数里被 crossinline 修饰的函数类型的参数,将不再享有「Lambda 表达式可以使用 return」的福利。所以这个 return 并不会面临「要结束谁」的问题,而是直接就不许这么写。

fun ppp(runnable: Runnable) {

}

inline fun hello(crossinline postAction: () -> Unit) {
    LogUtil.d("Hello!")
    ppp{
        postAction() 
    }
}

fun main(){
    hello {
        LogUtil.d("Bye!")
        return //这里编译不通过,提示:'return' is not allowed here
        LogUtil.d("Bye!2")
    }
}

也就是说,间接调用和 Lambda 的 return,你只能选一个。
所以什么时候需要 crossinline?当你需要突破内联函数的「不能间接调用参数」的限制的时候,但伴随着就要放弃Lambda中使用return了

7.总结

  • inline 可以让你用内联(也就是函数内容直插到调用处)的方式来优化代码结构,从而减少函数类型的对象的创建;
  • noinline 是局部关掉这个优化,来摆脱 inline 带来的「不能把函数类型的参数当对象使用」的限制;
  • crossinline 是局部加强这个优化,让内联函数里的函数类型的参数可以被间接调用

8.扩展:inline的另类用法,在函数里直接去调用 Java 的静态方法

在这里插入图片描述

用偷天换日的方式来去掉了这些 Java 的静态方法的前缀,让调用更简单:
在这里插入图片描述

这种用法不是 inline 被创造的初衷,也不是 inline 的核心意义,这属于一种相对偏门的另类用法。不过这么用没什么问题啊,因为它的函数体简洁,并不会造成字节码膨胀的问题。你如果有类似的场景,也可以这么用。




参考文章:
Kotlin 源码里成吨的 noinline 和 crossinline 是干嘛的?看完这个视频你转头也写了一吨

  • 27
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值