• Kotlin inline、noinline、crossinline 深入解析


    主要内容:

    • inline
      • 高价函数的原理分析
      • Non-local returns
    • noinline
    • crossinline

    inline

    如果有C语言基础的,inline 修饰一个函数表示该函数是一个内联函数。编译时,编译器会将内联函数的函数体拷贝到调用的地方。我们先看下在一个普通的 kotlin 函数上使用 inline 关键字:

    inline fun inlineFun() {
        println("from inlineFun")
    }
    
    • 1
    • 2
    • 3

    会发现 IDE 会给出警告:

    在这里插入图片描述
    建议我们在高阶函数上使用 inline 关键字。

    好,那我们来看下高阶函数。

    高价函数的原理分析

    下面是一个简单的高阶函数,函数参数是一个 function type 类型:

    private fun proxy(action: () -> Unit) {
        println("start logging")
        action()
        println("end logging")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    编译后对应的 Java 代码为:

    private final void proxy(Function0 action) {
      String var2 = "start logging";
      System.out.println(var2);
      action();
      var2 = "end logging";
      System.out.println(var2);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    会将 function type 编译成 Function0 类型,因为 action: () -> Unit括号内是无参的,所以是 Function0,如果是一个参数对应 Function1,以此类推。然后,我们调用上面的高阶函数 proxy:

    fun invokeProxy() {
        proxy {
            println("eating")
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    查看对应的字节码:

      public final invokeProxy()V
       L0
        LINENUMBER 34 L0
        ALOAD 0
        GETSTATIC inline/InlineTest$invokeProxy$1.INSTANCE : Linline/InlineTest$invokeProxy$1;
        CHECKCAST kotlin/jvm/functions/Function0
        INVOKESPECIAL inline/InlineTest.proxy (Lkotlin/jvm/functions/Function0;)V
       L1
        LINENUMBER 38 L1
        RETURN
       L2
        LOCALVARIABLE this Linline/InlineTest; L0 L2 0
        MAXSTACK = 2
        MAXLOCALS = 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    可以看出,编译后会生成一个内部类:inline/InlineTest$invokeProxy$1,然后我们看下这个内部类长什么样:

    final class inline/InlineTest$invokeProxy$1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function0 {
      // access flags 0x1041
      public synthetic bridge invoke()Ljava/lang/Object;
       L0
        LINENUMBER 15 L0
        ALOAD 0
        INVOKEVIRTUAL inline/InlineTest$invokeProxy$1.invoke ()V
        GETSTATIC kotlin/Unit.INSTANCE : Lkotlin/Unit;
        ARETURN
        MAXSTACK = 1
        MAXLOCALS = 1
    
      // access flags 0x11
      public final invoke()V
       L0
        LINENUMBER 36 L0
        LDC "eating"
        ASTORE 1
       L1
        GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
        ALOAD 1
        INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
       L2
       L3
        LINENUMBER 37 L3
        RETURN
       L4
        LOCALVARIABLE this Linline/InlineTest$invokeProxy$1; L0 L4 0
        MAXSTACK = 2
        MAXLOCALS = 2
    
      // access flags 0x0
      <init>()V
        ALOAD 0
        ICONST_0
        INVOKESPECIAL kotlin/jvm/internal/Lambda.<init> (I)V
        RETURN
        MAXSTACK = 2
        MAXLOCALS = 1
    
      // access flags 0x19
      public final static Linline/InlineTest$invokeProxy$1; INSTANCE
    
      // access flags 0x8
      static <clinit>()V
        NEW inline/InlineTest$invokeProxy$1
        DUP
        INVOKESPECIAL inline/InlineTest$invokeProxy$1.<init> ()V
        PUTSTATIC inline/InlineTest$invokeProxy$1.INSTANCE : Linline/InlineTest$invokeProxy$1;
        RETURN
        MAXSTACK = 2
        MAXLOCALS = 0
    
      @Lkotlin/Metadata;(mv={1, 8, 0}, k=3, d1={"\u0000\u0008\n\u0000\n\u0002\u0010\u0002\n\u0000\u0010\u0000\u001a\u00020\u0001H\n\u00a2\u0006\u0002\u0008\u0002"}, d2={"", "", "invoke"})
      OUTERCLASS inline/InlineTest invokeProxy ()V
      // access flags 0x18
      final static INNERCLASS inline/InlineTest$invokeProxy$1 null null
      // compiled from: InlineTest.kt
    }
    
    • 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
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59

    上面的字节码,类似下面的伪代码:

    final class InlineTest$invokeProxy$1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function0 {
     public final static InlineTest$invokeProxy$1 INSTANCE;
        
        static {
           INSTANCE = new InlineTest$invokeProxy$1()
        }
    
        public void invoke(){
         System.out.println("eating")
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    可以看出,调用高阶函数 proxy,会生成内部类(该内部类是一个单例)将lambda体里的代码,拷贝到 invoke 函数里面。

    小结:简单来说,就是有多少个调用点(call site 调用高阶函数的地方)就会产生多少个内部类。

    我们继续往下看,如果在 lambda 表达式体里访问外部的变量呢:

    class InlineTest {
        var age = 18
        private fun proxy(action: () -> Unit) {
            println("start logging")
            action()
            println("end logging")
        }
     fun invokeProxy() {
            proxy {
                age = 11 // 访问外部的成员变量
                println("eating")
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    invokeProxy对应的字节码如下:

      public final invokeProxy()V
       L0
        LINENUMBER 34 L0
        ALOAD 0
        NEW inline/InlineTest$invokeProxy$1
        DUP
        ALOAD 0
        INVOKESPECIAL inline/InlineTest$invokeProxy$1.<init> (Linline/InlineTest;)V
        CHECKCAST kotlin/jvm/functions/Function0
        INVOKESPECIAL inline/InlineTest.proxy (Lkotlin/jvm/functions/Function0;)V
       L1
        LINENUMBER 38 L1
        RETURN
       L2
        LOCALVARIABLE this Linline/InlineTest; L0 L2 0
        MAXSTACK = 4
        MAXLOCALS = 1
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    对应的 Java 伪代码如下:

    public final void invokeProxy(){
     InlineTest$invokeProxy$1 function0 = new InlineTest$invokeProxy$1()
        proxy(function0)
    }
    
    • 1
    • 2
    • 3
    • 4

    该内部类 InlineTest$invokeProxy$1 变成如下:

    final class InlineTest$invokeProxy$1 extends kotlin/jvm/internal/Lambda implements kotlin/jvm/functions/Function0 {
        public void invoke(){
            InlineTest$invokeProxy$1.this.setAge(11)
         System.out.println("eating")
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    可以看出每调用一次高阶函数 proxy 会都会创建一个内部类对象。

    小结:

    1. 每个调用高阶函数地方(调用点),编译时,编译器都会生成一个内部类
    2. 调用高阶函数时,如果传入的 lambda 表达式体没有使用外部变量,那么只会用到内部类常量对象;如果 lambda 表达式体使用了外部变量,那么每次调用该高阶函数都会创建一个内部类对象。
      如果在一个频繁触发的地方调用高阶函数,如自定义 draw 方法里,刚好 lambda 体又实用到了外部成员变量,这样就会隐性地在 draw 方法里频繁创建对象。

    这样时候 inline 关键字就派上用场了。

    将上面的高阶函数 proxy 使用 inline 修饰:

    private inline fun proxy(action: () -> Unit) {
        println("start logging")
        action()
        println("end logging")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    调用该高阶函数:

    fun invokeProxyInline() {
        proxy {
            println("eating")
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    字节码对应的 Java 代码如下:

    public final void invokeProxyInline() {
        int $i$f$proxyInline = false;
        String var3 = "start logging";
        System.out.println(var3);
        int var4 = false;
        String var5 = "eating";
        System.out.println(var5);
        var3 = "end logging";
        System.out.println(var3);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    可以看出调用 proxy 函数的地方不会创建内部类,而是将高阶函数的函数体拷贝到调用点。

    Non-local returns

    什么是 Non-local returns?我们先来看下什么是 return,下面是 kotlin 对 return 的定义:

    by default returns from the nearest enclosing function or anonymous function.
    
    • 1

    意思就是:从离 return 最近的封闭函数或匿名函数中返回。举个例子:

    fun test(age:Int) {
        if (age < 0) {
            return
        }
     println(age)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    其中 test就是 enclosing function. 也就是说 return 的作用就是从一个函数中返回(离它最近的函数)。

    搞清楚 return 关键字之后,我们来看下在 lambda 中使用 reutrn:

    // 定义一个普通的高阶函数
    private fun normalFunction(action: () -> Unit){
        println("start......")
        action()
        println("end......")
    }
    
    main(){
        // 调用高阶函数
        normalFunction {
            return // 使用 return, 编译器报错
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    发现编译器报错了,为啥不能在 lambda 中使用 return 呢?

    首先,上面代码中里离 return关键字最近的 enclosing function是 main函数,有人可能会问,离 return最近的不是 normalFunction么?normalFunction只是一个函数调用,它不是一个封闭的函数,封闭的是 lambda 表达式。

    其次,return也无法控制 normalFunction函数的 return的。因为 return所处的代码块只是 normalFunction的 lambda 参数而已,return控制的是 lambda。正如 Kotlin 官网所说的:

    A bare return is forbidden inside a lambda because a lambda cannot make the enclosing function return.
    
    • 1

    因为在 lambda 中使用return,无法实现 return 的定义,所以无法在 lambda 中使用 return,如果是内联函数,则可以在 lambda 中使用 return:

    private inline fun normalFunction(action: () -> Unit){
        println("start......")
        action()
        println("end......")
    }
    
    main(){
        // 调用高阶函数
        normalFunction {
            return
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    上面的代码是合法的(因为内联 normalFunction,编译时会将代码体拷贝到main函数中).

    Kotlin 中把这种 return 称之为 Non-local returns(located in a lambda, but exiting the enclosing function).
    Non-local returns 名字很好理解:return 的 local 是 lambda,而此处的 return 返回的是 lambda 外面的 main 函数(non-local),所以称之为 non-local returns.

    noinline

    noinline 顾名思义就是不内联。那是什么时候使用 noinline 呢?我们在上面 proxy 函数基础上做一个小修改:

    private inline fun proxy(action: () -> Unit, action2: () -> Unit) {
        println("start logging")
        action()
        println("end logging")
        // action2 作为参数传递给另一个高阶函数
        cleanResource(action2)
    }
    
    private fun cleanResource(execution: () -> Unit) {
        execution()
        println("cleaning resource1")
        println("cleaning resource2")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    为了 proxy 新增了另一个参数 action2,然后将 action2 传递给高阶函数 cleanResource. 但是 IDEA 会提示如下错误:
    在这里插入图片描述
    提示我们使用 noinline 修饰 action2 参数:

    private inline fun proxy(action: () -> Unit, noinline action2: () -> Unit) {
        println("start logging")
        action()
        println("end logging")
        // action2 作为参数传递给另一个高阶函数
        cleanResource(action2)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    为什么不加 noinline 就会报错呢?

    其实很好理解,因为 proxy 是一个 inline 函数,那么调用 proxy 的地方,编译器都会将函数体拷贝过去,包括传入的 lambda 参数(如上面的 action,action2),例如:

    fun invokeProxy() {
        proxy({
            println("eating...")
        }, {
            println("eating...2")
        })
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    action对应的代码块是:println("eating..."),action2 对应的代码块是:println("eating...2")

    因为action2传递给了 cleanResource,要想将代码块当做参数传递给函数,那么代码块用什么来表示,目前只能使用Class类。然而 proxy又是 inline 的,所以需要对 action2 参数单独处理,将其不要 inline。所以需要使用 oninline关键字来修饰 action2参数。

    小结:如果需要将 inline 高阶函数的 lambda 参数传递给另一个高阶函数或作为函数的返回值,均需要使用 noinline 关键字修饰该参数。

    crossinline

    介绍完了 inline 和 noinline,我们来看下 crossinline。将上面的 proxy 函数,稍作修改:

    private fun wrap(action: () -> Unit) {
        action.invoke()
    }
    
    private inline fun proxy(action: () -> Unit) {
        println("start logging")
        wrap {
            action()
        }
        println("end logging")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    编译器会报错,给出如下提示:

    在这里插入图片描述
    编译器报错原因:action 可能包含 Non-local returns,Kotlin 中不允许在非内联的 lambda 中使用 return(原因已经在 Non-local returns 章节已介绍了),也就是说 action 代码块中可能存在 return 关键字,需要使用 crossinline 来修饰 action 参数:

    private inline fun proxy(crossinline action: () -> Unit) {
        println("start logging")
        wrap {
            action()
        }
        println("end logging")
    }
    
    // 调用 proxy
    private fun invokeProxy() {
        proxy{
            println("invoke acrossinline")
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    我们看下 invokeProxy字节码:

    // 内部类
    public final class InlineTest$invokeProxy$$inlined$proxy$1 extends Lambda implements Function0 {
       public InlineTest$invokeProxy$$inlined$proxy$1() {
          super(0);
       }
    
       // $FF: synthetic method
       // $FF: bridge method
       public Object invoke() {
          this.invoke();
          return Unit.INSTANCE;
       }
    
       public final void invoke() {
          int var1 = false;
          String var2 = "invoke acrossinline";
          System.out.println(var2);
       }
    }
    
    private final void invokeProxy() {
        int $i$f$proxy = false;
        String var3 = "start logging";
        System.out.println(var3);
        // 每次都 new 一个内部类对象
        access$wrap(this, (Function0)(new InlineTest$invokeProxy$$inlined$proxy$1()));
        var3 = "end logging";
        System.out.println(var3);
    }
    
    • 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
    • 29

    可以看出会生成一个内部类

    • InlineTest$invokeProxy$$inlined$proxy$1

    并且每次调用都会创建一个内部类对象。crossinline 阻止了 action 参数内联。

    可以到看 crossinline 的核心作用是阻止内联,那我们将 crossinline 换成 noinline 是不是也可以呢?

    private inline fun proxy(noinline action: () -> Unit) {
        println("start logging")
        wrap {
            action()
        }
        println("end logging")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    编译器不会报错,代码运行也是OK,貌似可以使用 noinline 代替 crossinline,既然可以用 noinline,为啥还搞个新的 crossinline 关键字?上面的代码虽然可以使用 noinline 代替 crossinline,但是底层还差别的。我们看下使用 noinline 对应的字节码:

      public final invokeProxy()V
       L0
        LINENUMBER 135 L0
        ALOAD 0
        ASTORE 1
        GETSTATIC inline/InlineTest$invokeProxy$1.INSTANCE : Linline/InlineTest$invokeProxy$1;
        CHECKCAST kotlin/jvm/functions/Function0
        // 省略其他代码
       L5
        LINENUMBER 184 L5
        ALOAD 1
        NEW inline/InlineTest$proxy$1
        DUP
        ALOAD 2
        INVOKESPECIAL inline/InlineTest$proxy$1.<init> (Lkotlin/jvm/functions/Function0;)V
        CHECKCAST kotlin/jvm/functions/Function0
        INVOKESTATIC inline/InlineTest.access$wrap (Linline/InlineTest;Lkotlin/jvm/functions/Function0;)V
        // 省略其他...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    可见,使用 noinline 的话,会创建两个内部类:

    • inline/InlineTest$invokeProxy$1
    • inline/InlineTest$proxy$1

    至此,inline、noinline 和 crossinline 就介绍完毕了。

  • 相关阅读:
    stm32学习笔记:OLED显示屏
    苹果CMS海螺模版V20修复版/加广告代码 ​适合视频影视类网站使用​
    googleTest V1.12.1的基础用法
    mysql11,9学习笔记
    ntfs硬盘如何在mac上读写移动硬盘文件?
    私有云不是真正的云计算!
    网络层之IP协议(必备知识)
    图像领域-深度学习网络结构(从浅入深)——基础到对比到改进
    LinkedList底层结构与源码分析
    学生宿舍护眼台灯怎么样选择?适合宿舍使用的五款台灯
  • 原文地址:https://blog.csdn.net/johnny901114/article/details/132634727