简单理解 Kotlin 中的 inline 关键字
参考:
- Demystifying the inline keyword
- Kotlin Basics: Inline Functions
- Kotlin inline 关键字的使用
inline 这个词其实很好理解,翻译成中文就指内联。
相信每一个学习 Kotlin 的同学都有用过或者遇见过 inline
这个关键字。即便没有在 Kotlin 中见过那也不是什么问题,本文将带领你理解这个关键词。
实际上,如果你有学过C++,那你已经知道它是个什么玩意了。
本文就 Kotlin 中的 inline
进行讲解。
inline 是如何工作的
在此之前,我想先谈一谈 inline 与 lambda
Kotlin 在许多情况下使用编译器技巧来隐藏 Java 的旧语言结构。
在使用函数作为另一个函数的参数(所谓的高阶函数)比在Java中更自然。
虽然 Kotlin 好处多多,但在用 lambda 时有一些缺点。由于 lambda 表达式都会被悄悄的编译成一个匿名类。这也就意味着需要占用内存。如果短时间内 lambda 表达式被多次调用,大量的对象实例化就会产生内存流失(Memory Churn)。
所以为了避免这种情况,我们就可以使用 inline
inline fun inlined(getString: () -> String?) = println(getString())
fun notInlined(getString: () -> String?) = println(getString())
上面的代码中,一个是被 inline
修饰的内联函数,另一个不是。虽然他们的业务解决结果完全相同,都是打印getString()
,但如果你实际调用了这两个函数,从反编译后的Java代码中就可以看到很大的差异。
实际调用:
fun test() {
var testVar = "Test"
notInlined { testVar }
inlined { testVar }
}
相应的反编译的Java代码:
public static final void test() {
final ObjectRef testVar = new ObjectRef();
testVar.element = "Test Variable";
// notInlined:
notInlined((Function0)(new Function0(0) {
public Object invoke() {
return this.invoke();
}
@NotNull
public final String invoke() {
return (String)testVar.element;
}
}));
// inlined:
String var3 = (String)testVar.element;
System.out.println(var3);
从反编译的代码来看,我们并没有找到实际调用inlined
函数的迹象。
而 notInlined
使用 Function0
匿名类作为参数调用了该函数。为了遵守Function0
接口的协议所以就使用invoke()
实现了该方法。由于接口是通用的,因此生成另外的方法,即所谓的桥接方法。
通过上面的示例,你应该大概也看明白了
inline 的工作原理就是将内联函数的函数体复制到调用处实现内联。
inlined
函数内的(也就是 inlined
函数等号右边的) printlin()
,被拿去直接 System.out.println()
。
所以如果我们的方法比较大或者调用的比较多的话,那么编译器生成的代码量就会变大。
值得注意的是,如果方法变得足够大,过度使用 inline
可能会妨碍或停止 Hotspot 优化(例如方法内联)。默认情况下, Hotspot 不会内联大于35个字节的方法。
我们不应该内联所有功能。而且官方也不建议这样做。
在 kotlin-style-guide 这个库里的 issue 中找到个来自 Kotlin 贡献者的建议,
原文是:「Functions should only be made inline when they use inline-only features like inlined lambda parameters or reified types.」意思就是说:inline 关键字应该只用在需要内联特性的函数中,比如高阶函数作为参数和具体化的类型参数时。
当然了,官方也都是挺遵守这个规定的。比如在 Anko 库中,一个我很爱用的内联函数 bg
就有遵守着这个规定:
@PublishedApi
internal var POOL = newFixedThreadPoolContext(2 * Runtime.getRuntime().availableProcessors(), "bg")
inline fun <T> bg(crossinline block: () -> T): Deferred<T> = async(POOL) {
block()
}
要稍微注意一下的是 POOL
这个变量前面有 @PublishedApi
以及 internal
修饰着,这是因为内联函数体中不能直接访问到其外部类的成员,所以需要声明访问的成员为 internal
并且使用@PublishedApi
做注解。
相信看到这里,你已经对 inline 有了基本了解。
简介
kotlin 中,有三个类似的概念,inline
,noinline
和 crossinline
。平时使用的时候,很容易混淆。本文会介绍这三个概念的用法以及区别。
inline
inline
就是我们常说的内联。这个关键字会在编译期间起作用。如果一个函数是 inline
的,那么编译器会在编译的时候,把这个函数复制到调用处。这样做有什么好处呢?总的来说,好处有三个:
第一点,会减少函数调用的次数。我们知道,虽然函数调用的开销很小,但是确实是有一定的开销的。尤其是在大量的循环中,这种开销会变得更加明显。
比如如下代码:
// Kotlin
fun main(args: Array<String>) {
multiplyByTwo(5)
}
fun multiplyByTwo(num: Int) : Int {
return num * 2
}
他进行反编译之后的等价 Java 代码如下:
// Java
public static final void main(@NotNull String[] args) {
//...
multiplyByTwo(5);
}
public static final int multiplyByTwo(int num) {
return num * 2;
}
可以看到,不加 inline
的方法,编译成字节码,然后再反编译成等价 java 代码,得到的结果是一个普通的方法。这个跟我们的常识是吻合的。
但是,当我们把方法用 inline
修饰了之后,会发生什么呢?
比如如下代码中,我们把 multiplyByTwo
用 inline
参数修饰了一下:
// Kotlin
fun main(args: Array<String>) {
multiplyByTwo(5)
}
inline fun multiplyByTwo(num: Int) : Int {
return num * 2
}
反编译得到的结果如下:
// Java
public static final void main(@NotNull String[] args) {
// ...
int num$iv = 5;
int var10000 = num$iv * 2;
}
public static final int multiplyByTwo(int num) {
return num * 2;
}
可以看到,inline 中的方法,被复制到了调用方。这就是 inline 威力强大的地方!
第二点,会减少对象的生成。当方法中,有一个参数是 lambda
的时候,使用 inline
的方法,可以减少对象的生成。kotlin 对于默认的 lambda
参数的处理方式为,把 lambda
转化成一个类,看起来跟 java 中的匿名内部类非常相似。
比如,
// Kotlin
fun main(args: Array<String>) {
val methodName = "main"
multiplyByTwo(5) { result: Int -> println("call method $methodName, Result is: $result") }
}
fun multiplyByTwo(num: Int,
lambda: (result: Int) -> Unit): Int {
val result = num * 2
lambda.invoke(result)
return result
}
反编译之后的结果有点复杂:
public final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
final String methodName = "main";
this.multiplyByTwo(5, (Function1)(new Function1() {
public Object invoke(Object var1) {
this.invoke(((Number)var1).intValue());
return Unit.INSTANCE;
}
public final void invoke(int result) {
String var2 = "call method " + methodName + ", Result is: " + result;
boolean var3 = false;
System.out.println(var2);
}
}));
}
public final int multiplyByTwo(int num, @NotNull Function1 lambda) {
Intrinsics.checkParameterIsNotNull(lambda, "lambda");
int result = num * 2;
lambda.invoke(result);
return result;
}
观察生成的结果:java 生成了一个 Function1
类型的对象,来表示这个 lambda
。其中,Funtion1
中的 1 就代表这个 lambda 值需要一个参数。类似的,如果是不需要参数的,那么就是 Function0
。这个生成的结果,跟我们平时写 java 代码的时候使用的匿名内部类的方式是一样的。那么,可想而知,如果这个 lambda
是在一个循环中被调用的,那么就会生成大量的对象。
既然,inline
有如上的好处,那么是否有什么“坏处”,或者会造成我们使用不方便的地方呢?
首先是,对于一个 public
的 inline
方法,他不可以引用类的私有变量。比如:
private val happy = true
inline fun testNonPrivateField() {
println("happy = ${happy}")
}
如果这么写代码,编译器会对 happy 保存。道理也很简单:既然 inline 是在编译期间复制到调用方,那么自然就不能引用类的私有变量,因为调用方很大可能应该是“看不见”这个私有变量的。
其次,inline 方法会对流程造成非常隐晦的影响。
// Kotlin
fun main(args: Array<String>) {
println("Start of main")
multiplyByTwo(5) {
println("Result is: $it")
return
}
println("End of main")
}
// Java
public static final void main(@NotNull String[] args) {
String var1 = "Start of main";
System.out.println(var1);
int num$iv = 5;
int result$iv = num$iv * 2;
String var4 = "Result is: " + result$iv;
System.out.println(var4);
}
观察上面的两端代码,我们发现在反编译出来的 java 代码中,没有找到 “End of main”。为什么呢?原因其实很简单:根据我们前面知道的,inline
其实就是把代码在编译期间复制到调用方,因此,如果 lambda 中有 return 语句,那么也会被原样复制过去,进而,因为 lambda
中的 return
的影响,导致编译器认为后面的 “End of main” 其实是不能被访问到的代码,于是在编译期间给去掉了。
所以,小结一下:inline
关键字的作用,是把 inline
方法以及方法中的 lambda
参数在编译期间复制到调用方,进而减少函数调用以及对象生成。
不过,inline
关键字对于 lambda
的处理有的时候不是我们想要的。也就是,有时我们不想让 lambda
也被 inline
。那么有什么办法呢?这个时候就需要 noinline
关键字了。
noinline
noinline
修饰的是 inline
方法中的 lambda
参数。noinline
用于我们不想让 inline
特性作用到 inline
方法的某些 lambda
参数上的场景。
比如:
// Kotlin
fun main(args: Array<String>) {
val methodName = "main"
multiplyByTwo(5) {
result: Int -> println("call method $methodName, Result is: $result")
}
}
inline fun multiplyByTwo(
num: Int,
noinline lambda: (result: Int) -> Unit): Int {
val result = num * 2
lambda.invoke(result)
return result
}
反编译的结果是:
public final void main(@NotNull String[] args) {
Intrinsics.checkParameterIsNotNull(args, "args");
final String methodName = "main";
byte num$iv = 5;
Function1 lambda$iv = (Function1)(new Function1() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object var1) {
this.invoke(((Number)var1).intValue());
return Unit.INSTANCE;
}
public final void invoke(int result) {
String var2 = "call method " + methodName + ", Result is: " + result;
boolean var3 = false;
System.out.println(var2);
}
});
int $i$f$multiplyByTwo = false;
int result$iv = num$iv * 2;
lambda$iv.invoke(result$iv);
}
public final int multiplyByTwo(int num, @NotNull Function1 lambda) {
int $i$f$multiplyByTwo = 0;
Intrinsics.checkParameterIsNotNull(lambda, "lambda");
int result = num * 2;
lambda.invoke(result);
return result;
}
可以看到, 因为使用了 noinline
修饰了 lambda
,所以,编译器使用了匿名内部类的方式来处理这个 lambda,生成了一个 Function1
对象。
crossinline
是不是有了 inline
和 noinline
,对于我们开发人员来讲就够了呢?就满足了呢?显然不是的。考虑一种情况,我们既想让 lambda
也被 inline
,但是又不想让 lambda
对调用方的控制流程产生影响。这个产生影响,可以是有意识的主动控制,但是大多数情况下是开发人员的不小心导致的。我们知道 java 语言是一个编译型语言,如果能在编译期间对这种 inline
lambda
对调用方产生控制流程影响的地方进行提示甚至报错,就万无一失了。
crossinline
就是为了处理这种情况而产生的。crossinline
保留了 inline
特性,但是如果想在传入的 lambda
里面 return
的话,就会报错。return
只能 return
当前的这个 lambda
。
// Kotlin
fun main(args: Array<String>) {
val methodName = "main"
multiplyByTwo(5) {
result: Int -> println("call method $methodName, Result is: $result")
return@multiplyByTwo
}
}
如面代码所示,必须 return@multiplyByTwo
,而不能直接写 return
。
总结
inline
关键字的作用,是把 inline
方法以及方法中的 lambda
参数在编译期间复制到调用方,进而减少函数调用以及对象生成。对于有时候我们不想让 inline
关键字对 lambda
参数产生影响,可以使用 noline
关键字。如果想 lambda
也被 inline
,但是不影响调用方的控制流程,那么就要是用 crossinline
。