Kotlin 内联

Posted by OOFTF Blog on May 1, 2021

1. inline

1.1 inline 函数的目的

  1. 减少方法的调用,进而减少虚拟机栈的出栈入栈操作
  2. 优化 Lambda 开销

1.1.1 优化 Lambda 开销

在Kotlin中每次声明一个Lambda表达式,就会在字节码中产生一个匿名类。该匿名类包含了一个invoke方法,作为Lambda的调用方法,每次调用的时候,还会创建一个新的对象。可想而知,Lambda虽然简洁,但是会增加额外的开销。Kotlin 采用内联函数来优化Lambda带来的额外开销。

不使用内联函数

1
2
3
4
5
6
7
8
9
fun main(){
    testInline{
        print(it)
    }
}
private fun testInline(lam: (String) -> Unit) {
    lam.invoke("lambda")
    return
}

对标 Java 代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public final void main() {
    // 可以看到这里是生成了一个继承自 Function1 的类的单例
    testInline(MainActivity$main$1.INSTANCE);
}

private final void testInline(Function1<? super String, Unit> lam) {
    lam.invoke("lambda");
}

final class MainActivity$main$1 extends Lambda implements Function1<String, Unit> {
    public static final MainActivity$main$1 INSTANCE = new MainActivity$main$1();

    MainActivity$main$1() {
        super(1);
    }

    public Object invoke(Object p1) {
        invoke((String) p1);
        return Unit.INSTANCE;
    }

    public final void invoke(String it) {
        Intrinsics.checkNotNullParameter(it, "it");
        System.out.print(it);
    }
}    

使用内联函数

1
2
3
4
5
6
7
8
9
10
    fun main(){
        testInline{
            print(it)
        }
    }

    private inline fun testInline(lam: (String) -> Unit) {
        lam.invoke("lambda")
        return
    }

对标Java 代码

1
2
3
4
public final void main() {
   String it = "lambda";
   System.out.print(it);
}

1.2 inline 的实现原理

通过上面例子可以看出:inline 的工作原理就是将内联函数的函数体复制到调用处实现内联。

inline 的特性

  • 内联函数无法递归
  • 尽量避免对具有大量函数体的函数进行内联,这样会导致过多的字节码数量。
  • public 内联函数无法调用私有成员变量,因为其他类调用内联函数最终是要 copy 到目标类中,而其他类是无法使用私有变量的
  • 具体化参数类型
  • 内联函数也会将其 lambda 参数内联

具体化参数类型

内联函数可以帮助我们实现具体化参数类型,Kotlin与Java一样,由于运行时的类型擦除,我们不能直接获取一个参数的类型。然而,由于内联函数会直接在字节码中生成相应的函数体实现,这时候反而可以获得参数的具体类型。使用 reified 修饰符来实现这一效果。

1
2
3
4
5
6
7
8
9
fun main() {
    getType<Int>()
}

inline fun <reified T> getType() {
    // 非内联函数是无法直接使用 T::class 的
    println(T::class)
}

noinline

禁用内联,标记内联函数不需要内联的参数

我们的内联函数中有多个lambda 参数时,如果我们想要其中某一个lambda参数不进行内联,则可以使用noinline关键字。

1
2
3
4
5
6
7
8
9
10
11
12
    fun main() {
       myMethod(former = {
           println("former")
       }, latter = {
           println("latter")
       })
    }
    
    private inline fun myMethod(former: () -> Unit, noinline latter: () -> Unit) {
       former()
       latter()
    }

在上述情况下 latter 会被编译成一个Function 而不是直接将实现代码 copy 到调用处

crossinline

标记内联函数的 lambda 表达式参数,标识该 lambda 函数返回为非局部返回,不允许非局部控制流

在使用inline函数时,有一个注意点:小心地使用流程跳转。示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
fun main() {
   println("start execution:")
   sayHello {
       println("in lambda")
       return
   }
   println("end execution")
}
 
private inline fun sayHello(block: () -> Unit) {
   println("in sayHello")
   block()
}

//  输出结果为:
//  start execution:
//  in sayHello
//  in lambda

可以看出,最后的”end execution”并没有输出在控制台,那是因为程序在执行sayHello时,由于inline函数,导致return语句直接结束了main方法。我们如果想在sayHello定义时,就不想以后出现这样的使用隐患,就可以使用crossinline关键字:

1
2
3
4
private inline fun sayHello(crossinline block: () -> Unit) {
   println("in sayHello")
   block()
}

在加上了 crossinline 关键字之后,return 语句那块会显示语法错误。