Kotlin内联函数

Kotlin中内联函数的理解

kotlin中使用高阶函数会带来一些运行时的效率损失:每一个函数都是一个对象,并且会捕获一个闭包。即那些在函数体内会访问到的变量。内存分配(对于函数对象和类)和虚拟调用会引入运行时间开销。

调用一个方法是一个压栈和出栈的过程,调用方法时将栈针压入方法栈,然后执行方法体,方法结束时将栈针移出栈,这个压栈和出栈的过程会耗费资源,这个过程中传递形参也会耗费资源。

来看一个官方的例子:

1
2
3
4
5
6
7
8
fun <T> lock(l: Lock, body: () -> T): T {
l.lock()
try {
return body()
} finally {
l.unlock()
}
}

调用这个方法:

1
lock(l, {"do something!"})//l是一个Lock对象

对于编译器来说,调用lock方法就要将参数l和lambda表达式{“do something!”}进行传递,还要将lock方法进行压栈出栈处理,这个过程就会耗费资源。如果只要函数体类似这样:

1
2
3
4
5
6
l.lock()
try {
return "do something!"
} finally {
l.unlock()
}

这样做的效果和调用lock方法是一样的,而且不需要压栈出栈了,但是如果代码中频繁调用lock方法,必然要复制大量重复代码,那么有没有一种机制,又能少些重复代码(变成一个可供调用的方法)又不会在调用过程中频繁压栈出栈影响性能呢。有的,这就是kotlin的内联函数inline所拥有的能力。

inline

使用inline声明的函数,会在编译时将会拷贝到调用的地方。

inline function

定义一个sum函数计算两个数的和

1
2
3
4
5
6
7
fun main(args: Array<String>) {
println(sum(1, 2))
}

fun sum(a: Int, b: Int): Int {
return a + b
}

反编译为Java代码看看

1
2
3
4
5
6
7
8
public static final void main(@NotNull String[] args) {
int var1 = sum(1, 2);
System.out.println(var1);
}

public static final int sum(int a, int b) {
return a + b;
}

正常的样子,在该调用的地方调用函数。

然后为sum函数添加inline声明:

1
2
3
4
5
6
7
8
public static final void main(@NotNull String[] args) {
int var1 = sum(1, 2);
System.out.println(var1);
}

public static final inline sum(int a, int b) {
return a + b;
}

再反编译为Java代码:

1
2
3
4
5
6
7
8
9
10
11
public static final void main(@NotNull String[] args) {
//...
byte a$iv = 1;
int b$iv = 2;
int var4 = a$iv + b$iv;
System.out.println(var4);
}

public static final int sum(int a, int b) {
return a + b;
}

可以看到sum函数的实现代码被直接拷贝到了调用的地方。上面的例子其实并没有体现inline的优势,因为拷贝代码和在调用的地方调用方法没有本质区别,但是如果你的函数中有lambda形参数,或者是参数为函数的时候,inline的优势才会体现(因为不会新建函数对象,可以减少内存损耗)。

inline function with lambda parameters

再来看一个例子:

1
2
3
4
5
6
7
8
9
fun sum(a: Int, b: Int, lambda: (result: Int) -> Unit): Int { //sum方法中有一个函数参数
val r = a + b
lambda.invoke(r)
return r
}

fun main(args: Array<String>) {
sum(1, 2) { println("Result is: $it") }
}

反编译为java:

1
2
3
4
5
6
7
8
9
10
11
public static final int sum(int a, int b, @NotNull Function1 lambda) {
//...
int r = a + b;
lambda.invoke(r);
return r;
}

public static final void main(@NotNull String[] args) {
//...
sum(1, 2, (Function1)null.INSTANCE);
}

(Function1)null.INSTANCE,是由于反编译器工具在找不到等效的 Java 类时的显示的结果。

我们传递的那个lambda被转换为了Function1类型,它是Kotlin函数的一部分,它以1结尾是因为我们在lambda函数中传递了一个参数。

再来看一个代码:

1
2
3
4
5
fun main(args: Array<String>) {
for (i in 0..10) {
sum(1, 2) { println("Result is: $it") }
}
}

我们在循环中调用sum函数,每次都传递一个lambda函数打印结果,反编译为java:

1
2
3
for(byte var2 = 10; var1 <= var2; ++var1) {
sum(1, 2, (Function1)null.INSTANCE);
}

可见在每次循环里面都会创建一个Function1的实例对象,这里就是性能的优化点所在,如何优化呢?

  • 在循环外部建立lambda对象
1
2
3
4
5
val l: (r: Int) -> Unit = { println(it) }

for (i in 0..10) {
sum(1, 2, l)
}

反编译为java

1
2
3
4
5
6
Function1 l = (Function1)null.INSTANCE;
int var2 = 0;

for(byte var3 = 10; var2 <= var3; ++var2) {
sum(1, 2, l);
}

只会创建一个Function对象,优化了原来在循环内部不停创建对象。

  • 使用inline
1
2
3
4
5
6
7
8
9
10
11
fun main(args: Array<String>) {
for (i in 0..10) {
sum(1, 2) { println("Result is: $it") }
}
}

inline fun sum(a: Int, b: Int, lambda: (result: Int) -> Unit): Int {
val r = a + b
lambda.invoke(r)
return r
}

反编译java

1
2
3
4
5
6
7
8
9
10
11
12
public static final void main(@NotNull String[] args) {
//...
int var1 = 0;

for(byte var2 = 10; var1 <= var2; ++var1) {
byte a$iv = 1;
int b$iv = 2;
int r$iv = a$iv + b$iv;
String var9 = "Result is: " + r$iv;
System.out.println(var9);
}
}

lambda函数对象在编译的时候被拷贝到了调用的地方,避免了创建Fuction对象

inline使用注意事项

  • public inline函数不能访问私有属性
1
2
3
4
5
6
7
8
9
10
11
class Demo(private val title: String) {

inline fun test(l: () -> Unit) {
println("Title: $title") // 编译错误: Public-Api inline function cannot access non-Public-Api prive final val title
}

// 私有的没问题
private inline fun test(l: () -> Unit) {
println("Title: $title")
}
}
  • 注意程序控制流程

当使用inline的时候,如果传递给inline函数的lambda,有return语句,那么会导致闭包的调用者也返回。

看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
inline fun sum(a: Int, b: Int, lambda: (result: Int) -> Unit): Int {
val r = a + b
lambda.invoke(r)
return r
}

fun main(args: Array<String>) {
println("Start")
sum(1, 2) {
println("Result is: $it")
return // 这个会导致 main 函数 return
}
println("Done")//不会被执行
}

反编译java

1
2
3
4
5
6
7
8
9
public static final void main(@NotNull String[] args) {
String var1 = "Start";
System.out.println(var1);
byte a$iv = 1;
int b$iv = 2;
int r$iv = a$iv + b$iv;
String var7 = "Result is: " + r$iv;
System.out.println(var7);
}

反编译之后也能看到,lambda return之后的代码不会执行。(println(“Done”)没有执行)。怎么解决这个问题呢,不要使用return可以使用return@label语法,返回到lambda被调用的地方。

1
2
3
4
5
6
7
8
fun main(args: Array<String>) {
println("Start")
sum(1, 2) {
println("Result is: $it")
return@sum
}
println("Done")//会被执行
}

noinline

当一个inline函数中,有多个lambda函数做为参数的时候,可以在不想内联的lambda函数前使用noinline声明。不会被拷贝代码到被调用的地方。

看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
inline fun sum(a: Int, b: Int, lambda: (result: Int) -> Unit, noinline lambda2: (result: Int) -> Unit): Int {
val r = a + b
lambda.invoke(r)
lambda2.invoke(r)
return r
}

fun main(args: Array<String>) {
sum(1, 2,
{ println("Result is: $it") },
{ println("Invoke lambda2: $it") }
)
}

反编译为java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static final int sum(int a, int b, @NotNull Function1 lambda, @NotNull Function1 lambda2) {
int r = a + b;
lambda.invoke(r);
lambda2.invoke(r);
return r;
}

public static final void main(@NotNull String[] args) {
byte a$iv = 1;
byte b$iv = 2;
Function1 lambda2$iv = (Function1)null.INSTANCE;//lambda2新建了对象
int r$iv = a$iv + b$iv;
String var8 = "Result is: " + r$iv;
System.out.println(var8);
lambda2$iv.invoke(r$iv);
}

从反编译代码中可以看到使用inline声明的lambda形参函数被搬到了被调用的地方,而被noinline声明的函数则生成了Function对象。

crossinline

声明一个lambda函数不能有return语句(可以有return@label语句),这样可以避免使用inline时,lambda中的return影响流程导致某些语句没有执行。被crossinline声明的lambda函数如果有return语句会在编译时报错。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
inline fun sum(a: Int, b: Int, crossinline lambda: (result: Int) -> Unit): Int {
val r = a + b
lambda.invoke(r)
return r
}

fun main(args: Array<String>) {
sum(1, 2) {
println("Result is: $it")
return // 编译错误: return is not allowed here
}
}

总结

  • 使用 inline,内联函数到调用的地方,能减少函数调用造成的额外开销,在循环中尤其有效。
  • 使用 inline 能避免函数的 lambda 形参额外创建 Function 对象。
  • 使用 noinline 可以拒绝形参 lambda 内联。
  • 使用 crossinline 显示声明 inline 函数的形参 lambda 不能有 return 语句,避免lambda 中的 return 影响外部程序流程。