本文永久更新地址: https://xiaozhuanlan.com/topic/3458207169
重学 Kotlin 现已来到了第三期,前面现已介绍了:
object,史上最 “快” 单例 ?
typealias ,穿了马甲,我就不知道你了?
今天的主角是 inline ,这不是一个 Kotlin 特有的概念,大多数编程语言都支撑内联。
内联函数的语义很简单: 把函数体复制粘贴到函数调用处。运用起来也毫无困难,用 inli? z u 6ne
要害字修饰函数即可。
可是问题的要害并不是怎样运用 inline
,而是什么时分运用 inline
? 已然 Kotlin 供给了内联,它必定是为了功能优化而存在的,那么,它又真的是包治百病的功能良药吗?
今天,咱们就一起来刨根挖底,寻找一下答案。
目录
-
inline 的实质 -
主张我不要用 inline ? -
Java 支撑内联吗? -
解救 Lambda -
Java 是怎样优化Y V C W 9 7 f Lambda 的?h D 6 -
不想内联怎样办? -
怎样从 Lambda 回来? -
最终
inline 的实质
前^ & b o M面现已说过 inline
便是 把函数体复制粘贴到函数调用处,完全是编译器的小把戏。本着严谨科学的情绪,咱们仍是来反编译验证一下。
inline fun test() { println("I'mO r 7 a inline function")= I 8 , a W }
f# = C } ? s L @ ^un run() { test(3 C w I Y I s ! q) }
在 run()
函数中调用了内联函数 test()
。反编译检查对应的 java 代码:
public static final void test() {
String var1 = "I'm a inline function";
Sp v l D K / _ pystem.out.println(var1);
}
public static final void run() {
String var1 = "I'm a inline function";
System.o% D } , 4 0 lut.println(var1);
}
能够看到 run()
函数中并没有直接调用 test()
函数,而是把 test()
函数的代码直接放入自己的函数体中。这便是 inline
的功效。
那么,问题就来了。这样就能够提高运转功率吗?假如W z & ) 能够,为什么?
咱们先从 JVM 的办法履行机制说起。
JVM 进行办法调用和办法履@ I 3 * w G h J行依赖 栈帧,每一个办法从调用开端至履行完| _ . C结的进程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的进程。M U f . 6 s 1 ~ P
线程的栈帧是存储在虚拟机栈中,以上面示例代码的 未内联 版本为例,对应的办法履行进程和对应的栈帧结构如下所示:
未内联的状况下,整个履行进程中会发生两个办法栈帧,每一个办法栈帧都包~ T M { 2含了 部分f R s & ] W变量表、操作数栈、动态连接、办法回来地址和一些额定的附加信息。
运用内联的状况下,只需求一个办法栈帧,降低了办法调用的本钱。
乍一看,的确的提高了运转功率,究竟少用一个栈帧嘛。
可是?
主张不要运用 inline ?
一切看起来都很夸姣,除了 IDE 给我的刺眼提示。
Expected performance impact from inlining is insignificant. Inlining! l O y ^ works bG h 8 : C : uest for functions with parameters of functional types
大致意思是在这儿运用内联对功能的影响微乎其微,或者说没有什么含义。Kotlin 的内* 0 9 u |联最M l 8 y好用在函数参数类型中。
不急着解说,首先来一发魂灵拷问。
Java 支撑内联吗?
你能够说不支撑,由于 Java 并没有供给相似 inline
的显示声明内联函数的办法。
可是 JVM 是支撑的。Java 把内联优化交给虚拟q M Y [ k U机来进行,然后防止开发者的滥用。
典型的一种滥用,% o m v ^ z k _ J 内联超长办法,极大的增大字节码长度,反而得不偿失。你能够注意7 c / ) m y Kotlin 规范库中的内联函数,基本都是简略的函数。
关于一般的函数调用,JVM 现已供给了满足的内联支撑。因而,在 Kotlin 中,没有必要为一般函数运用内联,交给 JVM 就行了。
另外,Java 代码是由 javac
编译的U a ~ %,KoD 4 x 5 ! W # s Ktlin 代码是由 kotlinc
编译的,而 JVM 能够对字节码做统一的内联优化。所v H V g 5 .以,能够推断出,不管是 jav) h m 4ac ,仍是 kotlinc,在编译期是没有内联L u ! = ~ : | 优化的。
至于 JVM 具体的内联优化机制,我了解的并不多,这儿就不做过多介绍了。后续P V f | r假如我看到相t O 3 R { w关资料,会到这儿进行补充。7 E ? 6 ] 5 e W
所以,上一节中 IDE 给开发者的提示就很明了了。
JVM 现已供给了内联支撑,所以没有必要在 Kotlin 中内联一般函数。
那么问题又来了。@ + t 已然 JVM 现已支撑内联优化,Kotlin 的内联存在的含义是什么 ?答案便是 Lambda
。
解救 Lambda
为什么要解救 Lambda,咱们首先得知道Kotlin 的 Lambda 关于 JVM 而言究竟是什么。
Kotlin 规范库中有一个叫 runCatching
的函数,我在这儿完结一个简化版t X 0 F J % J ) h本 runCatch
,参数是一个函e B b数类型。
fun runCatch(block: ()P v 5 4 m s F . -> Unit){
try {
block()
}catch (e:Exception u 0){
e.printStackTrace()
}
}
fun run(){
runCQ C P . U 3 B y Uatch { println("xxx") }
}
反编译生成的 Java 代码如下所示:
public final class InlineKt {
public static final voidJ 2 v s v runCatch(@NotNull Fun1 E i x _ f i Tction0<Unit> block) {
Intrinsics.checkw = r C & VParameterIsNotNull(block, (String)"block");
try {
block.invoke();
}
catch (Exception e) {
e.printStackTrace();
}
}
public static final void run() {
InlineKt.runCatch((Function0<Unit&u o Y v K | h ; jgt;)((Function0)run.1.INSTANCE));
}
}
static final class InlineK N q 0 H J s y ~t.run.19 ~ ; & ; D g + extends Lambda implements Function0<Unit>D A 3 % M 9 : {
public static final InlineKt.run.1 INSTANCE = new /* invalid duplicate definition of idec t 2 *ntical inner class */;
pubb N Dlic final void invoke() {
String string = "xxx";
bo7 h .olean bl = false;
SysteP e Y i 6 Am.out.println((Object)string);
}
InlineKt.run.1() {
}
}
Kotlin 自诞生之初,就以 兼容 Java为首要方针。因而,Kotlin 关于 Lambda 表H @ ) & $ f f u ,达式的处理是编译生成匿名类。
经过编译器编译之后, runCatch()
办法中的 Lambda 参数被替换为 Function0<Unit>
类型,在 run()
办法中实践调用 runCatch()
时传入的参数是完结了 Function0<Unit>
接口H D L U p B的 InlineKt.run.1
,并重写 了 invoke()
办法。
所以,调用 runCatch()
的时分,会创建一个额定的类 InlineKt.run.1
。这是 Lambda 没有捕捉变量的场景。假如捕捉了变量,体现会怎样样?
fun runCatch(block: () -> Unit){
try {
block()
}catch (ek h V S | = ? t t:Exception){
e.prin7 j l i ( v $tStackTrace()
}
}
fun run(){
var messaO f V f &ge = "xxx"
runCatch { println(mes@ ; 5 4 y + |sage) }
}
在 Lambda 内部捕捉了外部变量 messx e a j m a $ ^ }age
,其对应的 javaG # ) 代码如下所示:
public^ i V # z M f, M ` I Vinal class InlineKt {
public static final void runCatch(@NotNull Function0<Unit> block) {
Intrinsics.checkParameterIsNotNull(block, (String)"block");
try {
block.invoke();
}
catch (Exceptionw + S e) {
e.printStackTrace()V z { F;
}
}
public static final void run() {
void message;
Ref.ObjectRef objectRef = new Ref.ObjectRef();
objectRef.elementJ t y 8 n _ 3 = "xxx";
// 这儿每次运转都会 new 一g # ~个对象
InlineKt.runCatch((Function0<Unit>)((Function0) m . $new Function0<Unit>((Ref.ObjectRef)message){
final /* synthetic */ Ref.ObjectRef $message;
public finaB r J g F ] A h Bl void invoke() {
String string = (String)this.$message.element;
boop E z 6 w Dlean bl = fa- B klse;
System.out.println((Objey N M e 4 l q act)string);
}
{
this.$message = o_ Y : z j RbjectRef;
super(0);
}
}));
}
}
假如 Lambda 捕捉了外部变量,那么每次运转都会 new 一个持有外部变量值的 Function0<Unit>
对象。这比未发生捕捉变量^ 6 x F W的状况更加糟糕。
总而言之,Kotlin 的 Ll 9 F FamW h 7 _ 6bda 为了完全兼容到 Java6,不只增大了编: W q译代码的体积,也带来了额定的运转时开支。为了处理这个问题,Kotlin 供给了 inline
要害字。
KotliJ h n E | o Zn 内联函数的作用是消除 lambda 带来的额M | p x ] = N L定开支r , | 6 s O
给 runCatch()
加持 inC O + o d `line :
inline fun runCatch(block: () -> Unit){
try {
block()
}catch (e:Exception){
e.6 - tprintStackTrace()
}
}
fun run(){
var message = "xxx"
runCatch { println(message) }
}
反编译检查 java 代码:
public sto T ,atic final void run() {
Object message =Y y o 9 % B "xxx";
boolean var1 =R a ` . # J v fq f 3 l ] J + c alse;
try {
int var2 = false;
SysteK q H 5m.out.println(me+ g n iss] W x L 0 Z } Bage);
} catch (Exception var5) {
var5.printStackTrace();
}
}
runCatch()
的代码被直接内联到 run(J { 8 J C G q T r)
办法中,没有额定生成其他类,消除了 Lambda 带来的额H & u m R w 9 6 –定开支。
Java 是怎样优化 Lambda 的?
已然 Kotlin 的 La7 ( j l & ) Z zmbda 存在功能问题,那周围的 Java 大兄弟必定也逃脱不了。
从 Java8 开端,Java 凭借 invokedH F G h s O y ! uynamic
来完结的 Lambda 的优化。
invokedynamic
用于支撑动态语言调用。在初次调用时,它会生成一个调用点,并绑定该调用点对应的办法句柄。后续调用时,直接运转该调用点对应的办法句柄即可。说直白一点,第一次调用 invokeddynamic
时,会找到此处应该运转的办法并绑定, 后续运转时就直接告知你这儿应该履行哪个办法。
关于 invokedynamic
的具体介绍,能够阅览极客时间专栏 《深入拆解t @ = 2 tJava虚拟机》的第 8,9 两讲。
JVM 是怎样a f ^ ,完结& u l invokedynamic 的?(上)
JVM 是怎样完结 invokedynamic 的?(下)
不想内联怎样办?
一个高阶函数一旦被符号为内联,它的办法体和一切 Lambda 参数都会被内联。
inline f~ U : nun test(block1i E e 4 & r: () -> Unit, block2: () -> Unit) {
blockN m [ 7 /1()
println("xxxp s Z o R m F s $")
block2()
}
test()
函数被符号为了 inline
,所以它的函数体以及两个 Lambda 参数都会被内联。可是m # I G由于我要传入的 block1
代码块巨长(或者q g o其他原因),我并不想将其内联,这时分就要运用 noinline
。
inline fun test(noinline block1: () -> Unit, block2: () -l j {> Unit) {
bl0 } 3 + 9 d p D _ock1()
println("xxx")
block2()
}
这样,9 k r c O N g e block1
就不z H {会被内联了。篇幅原因,这儿就不展现 Java 代码了,相信9 o { 你也能很简单了解 noinline
。
怎样从 Lambda 回来?
首先,一般的 lambda 是不答应直接运用 return
的 。
fun runCatch(block: () -> Unit) {
try {
print("bex - H [ Y 0 6fore lambda! : K P 1")
block()
print("after lambda")
} catch (e: Exception) {
e.printStackTrace()
}
}
fun run() {
// 一般 lambda 不答应 return
runCatch { return }
}
上面的代码没有办法经过编译,IDE 会提示你 return is not allowed here。 而 inline
能够让咱们打破这个约束。
// 符号为 inline
inline fun runCatch(block: () -> Unit) {
try {
print("before lambda")
block()
print("after lambda")
} catch (e: Exception) {
e.pr k kri[ A x H m R NntStackTrace()
}
}
fun run() {
runCatch { return }
}
上N g %面的代码是能够正常编译运转的。和之前的例子仅有的差异便是多了 { D ] Q e _ inline
。
已然答应 return
,那么这儿究竟是从 Lambda 中回来,持续运转后边的代码?仍是直接结束外层函数的运转呢?看一下 run()
办法的履行成果。
before lambda
从运转成果来看,是直接结束外层函数的运转。其实不难了解,这个 return 是直接内联到 run()
办法内部的,相当于在 run()
办法中直接调用 retud I | mrn
。从反编译的 java 代码看,一望而知。
public static final void run() {
boolean var0 = false;
try {
String var1 = "before lambda";
System.out.print(var1);
int var2 = false;
} catc` x W i . .h (Exception var3) {
varp l G . { 9 {3.printStackTrace();
}
}
编译器直接把 return
之后的代码优化掉了。这样的场景叫做 non-local return (非部分回来)。
可是有些时分我并不想直接退出外层函数,而是只是退出 Lambda 的运转,就能够这样写。
inline fun runCatch(block: () -J 6 / v F W ? o Q> Unit) {
try {
print("bW h `efore lambda")
block()
print("after lambda")
} ca_ G J k 8tch (c 7 g = = L 4 w ie: Exception) {
e.printStan r c # x C F eckTrace()
}
}
fun run() {
// 从 lambda 中回来
runCatch { return@runCatch }
}
return@label
,这样就会持续履行 Lambda 之后的代码了。这样的场景叫做 部分回来 。
还有一种场景,我是 API 的设计者,我不想 API 运用者进行非部分回来 ,改动我的代码流程。同时我又想运用 inline ,这样其实b C 6 J . Q l是抵触的。前面介绍过,内联z 2 v (会让 Lambda 答应非部分回来。
crossinliO j ` O j ` m #ne
便是为了处理这一抵触而生。a M *它能够在保e o Z d 5 } R ,持内联的状况下,制止 lambda 从外层函数直接回来d + d U。
inline fun runCatch(crossinline block: () -> Unitx { ^) {
try {
print("before lambda")
block()
print("after] r y ; i Y * [ P lambda")
} catB k [ ? s p 8 Hch (e: Exception)a ^ # ( {
e.printSt/ Y 5 m G $ $ =ackTrace()
}
}
fun run() {
runCatch { return }
}
增加 crossX # E 1 M vinline
之后,上面的代码将无法编译。但下面的代码仍然是能够编译运转的。
inline fun runCatch(crossinline block: () -> Unit) {
try {
print("before le g z I v ~ G + 9ambda")
block()
print("after lambda")
} catch (e: Excepti0 , f } , Won) {
e.printSp A E 2 a D HtackTrace()
}
}
fun run() {
runCatch { return@runCatch@ s 4 }
}
crossinline
能够阻挠非部分回来,但并不能阻挠部分回来,其实也没有必要。
最终
关于内联函数,一口气说了这么多,总结一下。
在 Kotlin 中,内联函数是用来补偿高阶函数中 Lambda 带来的额f S b p定运转开支的。关于一般函数,没有必要运用内联,由于 JVM 现已供给了一定的内联支撑。
对指定的 Lambda 参数运用 noinline
,能够防止该 Lambda 被内联。
一般的 Lambda 不支撑非部分回来,内联之后答应非部分回来。既要内联,又要制止非部分回来,请运用 cro9 v @ssij L _ B xnline
。
除了内联函数之外,Kotlin 1.3 开端支撑 inline class ,但这是一个实验性 API,需求手动敞开编译器支撑。不知道大家对内联类有什么共同的看法,欢迎在谈论区– $ L W Q +交流。
本文运用 mdnice 排版
这儿是秉心说,关注我,不迷路!