Kotlin 源码里成吨的 noinline 和 crossinline 是干嘛的?

Kotlin 源码里成吨的 noinline 和 crossinline 是干嘛的?,第1张

Kotlin 源码里成吨的 noinline 和 crossinline 是干嘛的?

刚才我说了,inline 关键字不止可以内联自己的内部代码,还可以内联自己内部的内部的代码,意思是什么呢,就是你的函数在被加了 inline 关键字之后,编译器在编译时不仅会把函数内联过来,而且会把它内部的函数类型的参数——那就是那些 Lambda 表达式——也内联过来。换句话说,这个函数被编译器贴过来的时候是完全展开铺平的:

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

天然的性能缺陷,我们通过 inline 关键字让函数用内联的方式进行编译,来减少参数对象的创建,从而避免出现性能问题。

所以,inline 是用来优化的吗?是,但你不能无脑使用它,你需要确定它可以带来优化再去用它,否则可能会变成负优化。其实换个角度想想:既然 inline 是优化,为什么 Kotlin 没有直接开启它,而要把它做成选项,而且还是个默认关闭的选项?就是因为它还真不一定是优化,加不加它需要我们自己去做判断。那怎么去做这个判断呢?很简单,如果你写的是高阶函数,会有函数类型的参数,加上 inline 就对了。

嗯……不过如果你们团队对于包大小有非常极致的追求,也可以选择酌情使用 inline,比如对代码做严格要求,只有会被频繁调用的高阶函数才使用 inline——这个可能在实施上会有点难度,一般来说,按我刚才说的原则就已经够了。

另外,Ko

《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》

【docs.qq.com/doc/DSkNLaERkbnFoS0ZF】 完整内容开源分享

tlin 的官方源码里还有一个 inline 的另类用法:在函数里直接去调用 Java 的静态方法:

用偷天换日的方式来去掉了这些 Java 的静态方法的前缀,让调用更简单:

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

讲到这儿……应该知道内联函数怎么用了吧?那我们就……继续深入一下?

noinline


说完 inline,我们来说另一个关键字:noinline。noinline 的意思很直白:inline 是内联,而 noinline 就是不内联。不过它不是作用于函数的,而是作用于函数的参数:对于一个标记了 inline 的内联函数,你可以对它的任何一个或多个函数类型的参数添加 noinline 关键字:

添加了之后,这个参数就不会参与内联了:

好理解吧?好理解是好理解,(皱眉)可是这有什么用啊?为什么要关闭这种优化?

首先我们要知道,函数类型的参数,它本质上是个对象。我们可以把这个对象当做函数来调用,这也是最常见的用法:

但同时我们也可以把它当做对象来用。比如把它当做返回值:

但当我们把函数进行内联的时候,它内部的这些参数就不再是对象了,因为他们会被编译器拿到调用处去展开。也就是说,当你的函数被这样调用的时候:

代码会被这样编译:

哎?请问你找谁啊?

发现问题了吗?当一个函数被内联之后,它内部的那些函数类型的参数就不再是对象了,因为它们的壳被脱掉了。换句话说,对于编译之后的字节码来说,这个对象根本就不存在。一个不存在的对象,你怎么使用?

所以当你要把一个这样的参数当做对象使用的时候,Android Studio 会报错,告诉你这没法编译:

那……我如果真的需要用这个对象怎么办?加上 noinline:

加了 noinline 之后,这个参数就不会参与内联了:

那我们就也可以正常使用它了。

所以,noinline 的作用是什么?是用来局部地、指向性地关掉函数的内联优化的。既然是优化,为什么要关掉?因为这种优化会导致函数中的函数类型的参数无法被当做对象使用,也就是说,这种优化会对 Kotlin 的功能做出一定程度的收窄。而当你需要这个功能的时候,就要手动关闭优化了。这也是 inline 默认是关闭、需要手动开启的另一个原因:它会收窄 Kotlin 的功能。

那么,我们应该怎么判断什么时候用 noinline 呢?很简单,比 inline 还要简单:你不用判断,Android Studio 会告诉你的。当你在内联函数里对函数类型的参数使用了风骚 *** 作,Android Studio 拒绝编译的时候,你再加上 noinline 就可以了。

crossinline


最后再来说 crossinline。这是个很有意思的关键字,刚才讲的 noinline 是局部关闭内联优化对吧?而这个 crossinline,是局部加强内联优化。

我们先来看代码。这里有一个内联函数,还有一个对它的调用:

假如我往这个 Lambda 表达式里加一个 return:

这个 return 会结束哪个函数的执行?是它外面的 hello() 还是再往外一层的 main()?

按照通常的规则,肯定是结束 hello() 的对吧?因为 hello() 离它近啊,return 所结束的肯定是直接包裹住它的那个函数。可是大家想一想,这个 hello() 是个内联函数对不对?内联函数在编译优化之后会怎么样?会被铺平是不是?而这个调用,在铺平后会变成这样:

那你再看看,return 结束的是哪个函数?是外层的对吧?也就是说,对于内联函数,它的参数中 Lambda 的 return 结束的不是这个内联函数,而是那个调用这个内联函数的更外层的函数。是这个道理吧!

道理是这个道理,但这就有问题了。什么问题?我一个 return 结束哪个函数,竟然要看这个函数是不是内联函数!那岂不是我每次写这种代码都得钻到原函数里去看看有没有 inline 关键字,才能知道我的代码会怎么执行?那这也太难了吧!

这种不一致性会给我们带来极大困扰,因此 Kotlin 制定了一条规则:Lambda 表达式里不允许使用 return,**除非——**这个 Lambda 是内联函数的参数。

那这样的话规则就简单了:

  1. Lambda 里的 return,结束的不是直接的外层函数,而是外层再外层的函数;

  2. 但只有内联函数的 Lambda 参数可以使用 return。

注:Lambda 可以用 return@label 的方式来显式指定返回的位置,但这个不是今天讨论的内容。

这样就既消了歧义,也避免了需要反复查看每个函数是不是内联函数的麻烦。

不过……我们如果把事情再变复杂一点——最后一次了,不会更复杂了:

这次,我用 runonUiThread() 把这个参数放在了主线程执行,这是一种很常见的 *** 作。

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

这就表示什么?当内联函数的 Lambda 参数在函数内部是间接调用的时候,Lambda 里面的 return 会无法按照预期的行为进行工作。

这就比较严重了,因为这造成了 Kotlin 这个语言的稳定性的问题了。结果是不可预测的,这能行吗,是吧?

那怎么办?

Kotlin 的选择依然是霸气一刀切:内联函数里的函数类型的参数,不允许这种间接调用。

解决了!解决不了问题,我就解决提出问题的人。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5683131.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存