Kotlin的内联函数

一、内联数的语义很简单

把函数体复制粘贴到函数调用处 。使用起来也毫无困难,用 inline关键字修饰函数即可。

inline fun test() {
    println("I'm a inline function")
}

fun run() { test() }

反编译后:

public static final void test() {
    String var1 = "I'm a inline function";
    System.out.println(var1);
}

public static final void run() {
    String var1 = "I'm a inline function";
    System.out.println(var1);
}

可以看到 run() 函数中并没有直接调用 test() 函数,而是把 test() 函数的代码直接放入自己的函数体中。这就是 inline 的功效。

意义:
JVM 进行方法调用和方法执行依赖 栈帧,每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里从入栈到出栈的过程。

未内联的情况下,整个执行过程中会产生两个方法栈帧,每一个方法栈帧都包括了 局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息 。

使用内联的情况下,只需要一个方法栈帧,降低了方法调用的成本。

Java 支持内联吗?

你可以说不支持,因为 Java 并没有提供类似 inline 的显示声明内联函数的方法。
但是 JVM 是支持的。Java 把内联优化交给虚拟机来进行,从而避免开发者的滥用。

对于普通的函数调用,JVM 已经提供了足够的内联支持。因此,在 Kotlin 中,没有必要为普通函数使用内联,交给 JVM 就行了。
另外,Java 代码是由 javac 编译的,Kotlin 代码是由 kotlinc 编译的,而 JVM 可以对字节码做统一的内联优化。所以,可以推断出,不管是 javac ,还是 kotlinc,在编译期是没有内联优化的。


既然 JVM 已经支持内联优化,Kotlin 的内联存在的意义是什么 ?

拯救 Lambda:

Kotlin 标准库中有一个叫 runCatching 的函数,我在这里实现一个简化版本 runCatch ,参数是一个函数类型

fun runCatch(block: () -> Unit){
    try {
        block()
    }catch (e:Exception){
        e.printStackTrace()
    }
}

fun run(){
    runCatch { println("xxx") }
}

反编译生成的 Java 代码如下所示:

public final class InlineKt {
    public static final void runCatch(@NotNull Function0<Unit> block) {
        Intrinsics.checkParameterIsNotNull(block, (String)"block");
        try {
            block.invoke();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static final void run() {
        InlineKt.runCatch((Function0<Unit>)((Function0)run.1.INSTANCE));
    }
}

static final class InlineKt.run.1 extends Lambda implements Function0<Unit> {
    public static final InlineKt.run.1 INSTANCE = new /* invalid duplicate definition of identical inner class */;

    public final void invoke() {
        String string = "xxx";
        boolean bl = false;
        System.out.println((Object)string);
    }

    InlineKt.run.1() {
    }
}

Kotlin 自诞生之初,就以 兼容 Java 为首要目标。因此,Kotlin 对于 Lambda 表达式的处理是编译生成匿名类。
经过编译器编译之后, runCatch() 方法中的 Lambda 参数被替换为 Function0<Unit> 类型,在 run() 方法中实际调用 runCatch()  时传入的参数是实现了 Function0<Unit>  接口的 InlineKt.run.1  ,并重写 了 invoke() 方法。
所以,调用 runCatch() 的时候,会创建一个额外的类 InlineKt.run.1。这是 Lambda 没有捕捉变量的场景。如果捕捉了变量,表现会怎么样?

fun runCatch(block: () -> Unit){
    try {
        block()
    }catch (e:Exception){
        e.printStackTrace()
    }
}

fun run(){
    var message = "xxx"
    runCatch { println(message) }
}

在 Lambda 内部捕捉了外部变量 message ,其对应的 java 代码如下所示:

public final class InlineKt {
    public static final void runCatch(@NotNull Function0<Unit> block) {
        Intrinsics.checkParameterIsNotNull(block, (String)"block");
        try {
            block.invoke();
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static final void run() {
        void message;
        Ref.ObjectRef objectRef = new Ref.ObjectRef();
        objectRef.element = "xxx";
        // 这里每次运行都会 new 一个对象
        InlineKt.runCatch((Function0<Unit>)((Function0)new Function0<Unit>((Ref.ObjectRef)message){
            final /* synthetic */ Ref.ObjectRef $message;

            public final void invoke() {
                String string = (String)this.$message.element;
                boolean bl = false;
                System.out.println((Object)string);
            }
            {
                this.$message = objectRef;
                super(0);
            }
        }));
    }
}

如果 Lambda 捕捉了外部变量,那么每次运行都会 new 一个持有外部变量值的 Function0<Unit>  对象。这比未发生捕捉变量的情况更加糟糕。
总而言之,Kotlin 的 Lambda 为了完全兼容到 Java6,不仅增大了编译代码的体积,也带来了额外的运行时开销。为了解决这个问题,Kotlin 提供了 inline 关键字。

Kotlin 内联函数的作用是消除 lambda 带来的额外开销

给 runCatch() 加持 inline :

inline fun runCatch(block: () -> Unit){
    try {
        block()
    }catch (e:Exception){
        e.printStackTrace()
    }
}

fun run(){
    var message = "xxx"
    runCatch { println(message) }
}

反编译查看 java 代码:

public static final void run() {
      Object message = "xxx";
      boolean var1 = false;
      try {
         int var2 = false;
         System.out.println(message);
      } catch (Exception var5) {
         var5.printStackTrace();
      }
}

runCatch() 的代码被直接内联到 run() 方法中,没有额外生成其他类,消除了 Lambda 带来的额外开销。

noinline

不想内联怎么办?
一个高阶函数一旦被标记为内联,它的方法体和所有 Lambda 参数都会被内联。

inline fun test(block1: () -> Unit, block2: () -> Unit) {
    block1()
    println("xxx")
    block2()
}

test() 函数被标记为了 inline  ,所以它的函数体以及两个 Lambda 参数都会被内联。但是由于我要传入的 block1  代码块巨长(或者其他原因),我并不想将其内联,这时候就要使用 noinline  。

inline fun test(noinline block1: () -> Unit, block2: () -> Unit) {
    block1()
    println("xxx")
    block2()
}

如果在内联函数的方法体内被其他非内联函数调用,就会报错

inline fun <T> mehtod(lock: Lock, body: () -> T): T {
            lock.lock()
            try {
                otherMehtod(body)//会报错
                return body()
            } finally {
                lock.unlock()
            }
    }

    fun <T> otherMehtod(body: ()-> T){

    }

原因:因为method是内联函数,所以它的形参也是inline的,所以body就是inline的,但是在编译时期,body已经不是一个函数对象,而是一个具体的值,然而otherMehtod却要接收一个body的函数对象,所以就编译不通过了
解决方法:当然就是加noinline了,它的作用就已经非常明显了.就是让内联函数的形参函数不是内联的,保留原有的函数特征.

如何从 Lambda 返回?

crossinline

首先,普通的 lambda 是不允许直接使用 return 的 。

fun runCatch(block: () -> Unit) {
    try {
        print("before lambda")
        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.printStackTrace()
    }
}

fun run() {
    runCatch { return }
}

上面的代码是可以正常编译运行的。和之前的例子唯一的区别就是多了 inline 。
既然允许 return ,那么这里究竟是从 Lambda 中返回,继续运行后面的代码?还是直接结束外层函数的运行呢?看一下 run() 方法的执行结果。

before lambda

从运行结果来看,是直接结束外层函数的运行。其实不难理解,这个 return 是直接内联到 run() 方法内部的,相当于在 run() 方法中直接调用 return。从反编译的 java 代码看,一目了然。

public static final void run() {
      boolean var0 = false;

      try {
         String var1 = "before lambda";
         System.out.print(var1);
         int var2 = false;
      } catch (Exception var3) {
         var3.printStackTrace();
      }
   }

编译器直接把 return 之后的代码优化掉了。这样的场景叫做 non-local return (非局部返回) 。
但是有些时候我并不想直接退出外层函数,而是仅仅退出 Lambda 的运行,就可以这样写。

inline fun runCatch(block: () -> Unit) {
    try {
        print("before lambda")
        block()
        print("after lambda")
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

fun run() {
    // 从 lambda 中返回
    runCatch { return@runCatch }
}

return@label,这样就会继续执行 Lambda 之后的代码了。这样的场景叫做 局部返回  。

还有一种场景,我是 API 的设计者,我不想 API 使用者进行非局部返回 ,改变我的代码流程。同时我又想使用 inline ,这样其实是冲突的。前面介绍过,内联会让 Lambda 允许非局部返回。

crossinline  就是为了解决这一冲突而生。它可以在保持内联的情况下,禁止 lambda 从外层函数直接返回。

inline fun runCatch(crossinline block: () -> Unit) {
    try {
        print("before lambda")
        block()
        print("after lambda")
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

fun run() {
    runCatch { return }
}

添加 crossinline 之后,上面的代码将无法编译。但下面的代码仍然是可以编译运行的。

inline fun runCatch(crossinline block: () -> Unit) {
    try {
        print("before lambda")
        block()
        print("after lambda")
    } catch (e: Exception) {
        e.printStackTrace()
    }
}

fun run() {
    runCatch { return@runCatch }
}

crossinline 可以阻止非局部返回,但并不能阻止局部返回,其实也没有必要

最后

关于内联函数,一口气说了这么多,总结一下。

1、在 Kotlin 中,内联函数是用来弥补高阶函数中 Lambda 带来的额外运行开销的。对于普通函数,没有必要使用内联,因为 JVM 已经提供了一定的内联支持。
2、对指定的 Lambda 参数使用 noinline ,可以避免该 Lambda 被内联。
3、普通的 Lambda 不支持非局部返回,内联之后允许非局部返回。既要内联,又要禁止非局部返回,请使用 crossinline 。
4、除了内联函数之外,Kotlin 1.3 开始支持 inline class ,但这是一个实验性 API,需要手动开启编译器支持。

链接:https://juejin.cn/post/6844904201353429006

类似文章

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注