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

inline


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

编译时常量为什么这么多限制?因为只有符合这些限制,编译器和 JVM 才有能力做优化,从而这种内联操作也才有意义。稍微复杂一点,就优化不动了。什么叫「稍微复杂」我不知道,但是函数内联这种操作,绝对算得上是相当复杂了,绝对优化不动的。其实真要较真起来,函数的内联确实会产生一种被动的优化,就是刚才我说的:去掉一个函数,调用栈少了一层,性能的损耗肯定会少一些,但实际上调用栈本身所造成的性能损耗本来就是非常小的,这个优化跟没优化差不多。这个事实可能不太符合我们的直觉,但你这样想一下:在我们看到的各种性能优化规范里,你有没有见过类似「少写几个方法来减少调用栈」这样的优化策略?没有吧?为什么?因为这种优化没有意义。而同时,函数内联不同于常量内联的地方在于,函数体通常比常量复杂多了,而函数内联会导致函数体被拷贝到每个调用处,如果函数体比较大而被调用处又比较多,就会导致编译出的字节码变大很多。我们都知道编译结果的压缩是应用优化的一大指标,而函数内联对于这项指标是明显不利的。所以靠 inline 来做性能优化?不存在的。

那么问题就来了:inline 是干嘛用的呢?

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

例如我把 hello() 函数的定义改成这样,给它增加一个函数类型的参数:

相应地,在调用处也需要填上这个参数。

我可以填成匿名函数的形式:

也可以简单点,写成 Lambda 表达式:

因为 Java 并没有对函数类型的变量的原生支持,Kotlin 需要想办法来让这种自己新引入的概念在 JVM 中落地。而它想的办法是什么呢?就是用一个 JVM 对象来作为函数类型的变量的实际载体,让这个对象去执行实际的代码。也就是说,在我对代码做了刚才那种修改之后,程序在每次调用 hello() 的时候都会创建一个对象来执行 Lambda 表达式里的代码,虽然这个对象是用一下之后马上就被抛弃,但它确实被创建了。

这有什么坏处?其实一般情况下也没什么坏处,多创建个对象算什么?但是你想一下,如果这种函数被放在循环里执行:

内存占用是不是一下就飚起来了?而且关键是,你作为函数的创建者,并不知道、也没法规定别人在什么地方调用这个函数,也就是说,这个函数是否出现在循环或者界面刷新之类的高频场景里,是完全不可控的。这样一来……这一类函数就全都有了性能隐患了。高阶函数是 Kotlin 相比起 Java 很方便的一个特性,但却有这么一个性能隐患,这……还让人怎么放心用啊?

这就是 inline 关键字出场的时候了。

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

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

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

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

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

另外,Kotlin 的官方源码里还有一个 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 的方式来显式指定返回的位置,但这个不是今天讨论的内容。

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

小结

有了这么多优秀的开发工具,可以做出更高质量的Android应用。

当然了,“打铁还需自身硬”,想要写出优秀的代码,最重要的一点还是自身的技术水平,不然用再好的工具也不能发挥出它的全部实力。

在这里我也分享一份大佬自己收录整理的Android学习PDF+架构视频+面试文档+源码笔记,还有高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料这些都是我闲暇还会反复翻阅的精品资料。在脑图中,每个知识点专题都配有相对应的实战项目,可以有效的帮助大家掌握知识点。

总之也是在这里帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值