Lambda - 认识java lambda与kotlin

Lambda这个估计算是一个非常有历史感的话题了,Lambda相关的文章,也有很多了,为啥还要拿出来炒炒冷饭呢?主要是最近有对Lambda的内容进行字节码处理,同时Lambda在java/kotlin/android中,都有着不一样是实现,非常有趣,因此本文算是一个记录,让我们一起去走进lambda的世界吧。当然,本文以java/kotlin视角去记录,在android中lambda的处理还不一样,我们先挖个坑,看看有没有机会填上,当然,部分的我也会夹杂的一起说!最简单的例子比如我们常常在写ui的时候,设置一个监听器,就是这么处理view.setOnClickListener(v -> {Log.e("hello","123");});复制代码编译后的字节码INVOKEDYNAMIC onClick()Landroid/view/View$OnClickListener; [// handle kind 0x6 : INVOKESTATICjava/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;// arguments:(Landroid/view/View;)V,// handle kind 0x6 : INVOKESTATIC这里就是我们要的方法类名.lambda$myFunc$0(Landroid/view/View;)V,(Landroid/view/View;)V复制代码emmm,密密麻麻,我们先不管这个,这里主要是INVOKEDYNAMIC的这个指令,这里我就不再重复INVOKEDYNAMIC的由来之类的了,我们直接来看,INVOKEDYNAMIC指令执行后的产物是啥?生成产物类首先产物之一,肯定是setOnClickListener里面需要的一个实现OnClickListener的对象对吧!我们都知道INVOKEVIRTUAL会在操作数栈的执行一个消耗“对象”的操作,这个从哪里来,其实也很明显,就是从INVOKEDYNAMIC执行后被放入操作数栈的。#bytemd-mermaid-1678846384283-0{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#bytemd-mermaid-1678846384283-0 .error-icon{fill:#552222;}#bytemd-mermaid-1678846384283-0 .error-text{fill:#552222;stroke:#552222;}#bytemd-mermaid-1678846384283-0 .edge-thickness-normal{stroke-width:2px;}#bytemd-mermaid-1678846384283-0 .edge-thickness-thick{stroke-width:3.5px;}#bytemd-mermaid-1678846384283-0 .edge-pattern-solid{stroke-dasharray:0;}#bytemd-mermaid-1678846384283-0 .edge-pattern-dashed{stroke-dasharray:3;}#bytemd-mermaid-1678846384283-0 .edge-pattern-dotted{stroke-dasharray:2;}#bytemd-mermaid-1678846384283-0 .marker{fill:#333333;stroke:#333333;}#bytemd-mermaid-1678846384283-0 .marker.cross{stroke:#333333;}#bytemd-mermaid-1678846384283-0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#bytemd-mermaid-1678846384283-0 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#bytemd-mermaid-1678846384283-0 .cluster-label text{fill:#333;}#bytemd-mermaid-1678846384283-0 .cluster-label span{color:#333;}#bytemd-mermaid-1678846384283-0 .label text,#bytemd-mermaid-1678846384283-0 span{fill:#333;color:#333;}#bytemd-mermaid-1678846384283-0 .node rect,#bytemd-mermaid-1678846384283-0 .node circle,#bytemd-mermaid-1678846384283-0 .node ellipse,#bytemd-mermaid-1678846384283-0 .node polygon,#bytemd-mermaid-1678846384283-0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#bytemd-mermaid-1678846384283-0 .node .label{text-align:center;}#bytemd-mermaid-1678846384283-0 .node.clickable{cursor:pointer;}#bytemd-mermaid-1678846384283-0 .arrowheadPath{fill:#333333;}#bytemd-mermaid-1678846384283-0 .edgePath .path{stroke:#333333;stroke-width:1.5px;}#bytemd-mermaid-1678846384283-0 .flowchart-link{stroke:#333333;fill:none;}#bytemd-mermaid-1678846384283-0 .edgeLabel{background-color:#e8e8e8;text-align:center;}#bytemd-mermaid-1678846384283-0 .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#bytemd-mermaid-1678846384283-0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#bytemd-mermaid-1678846384283-0 .cluster text{fill:#333;}#bytemd-mermaid-1678846384283-0 .cluster span{color:#333;}#bytemd-mermaid-1678846384283-0 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80,100%,96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#bytemd-mermaid-1678846384283-0:root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#bytemd-mermaid-1678846384283-0 flowchart{fill:apa;}INVOKEDYNAMIC生出来了OnClickListenerINVOKEVIRTUAL消耗当然,这个生成的类还是比较难找的,可以通过以下明=命令去翻翻java -Djdk.internal.lambda.dumpProxyClasses 类路径复制代码当然,在AS中也有相关的生成类,在intermediates/transform目录下,不过高版本的我找不到在哪了,如果知道的朋友也可以告诉一下调用特定方法我们的产物类有了,但是我们也知道,lambda不是生成一个对象那么简单,而是要调用到里面的闭包方法,比如我们本例子就是v -> {Log.e("hello","123");}复制代码那么我们这个产物的方法在哪呢?回到INVOKEDYNAMIC指令的里面,我们看到java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;// arguments:(Landroid/view/View;)V,// handle kind 0x6 : INVOKESTATIC类名.lambda$myFunc$0(Landroid/view/View;)V,(Landroid/view/View;)V]INVOKEVIRTUAL android/view/View.setOnClickListener (Landroid/view/View$OnClickListener;)V复制代码这里有很多新的东西,比如LambdaMetafactory(java创建运行时类),MethodHandles等,相关概念我不赘述啦!因为有比我写的更好的文章,大家可以观看一下噢!ASM对匿名内部类、Lambda及方法引用的Hook研究我这里特地拿出来INVOKESTATIC 类名.lambda$myFunc$0(Landroid/view/View;)V复制代码这里会在生成的产物类中,直接通过INVOKESTATIC方式(当然,这里只针对我们这个例子,后面会继续有说明,不一定是通过INVOKESTATIC方式)方法是lambdamyFuncmyFuncmyFunc0,我们找下这个方法,可以看到,还真的有,如下private static synthetic lambda$myFunc$0(Landroid/view/View;)VL0LINENUMBER 14 L0LDC "hello"LDC "123"INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)IPOP

}复制代码这个方法就是lambda要执行的方法,只不过在字节码中包装了一层。至此,我们就能够大概明白了,lambda究竟干了些什么java lambda vs Koltin lambdajava lambda我们刚刚有提到,生成的产物方法不一定通过INVOKESTATIC的方式调用,这也间接说明了,我们的lambda的包装方法,不一定是static,即不一定是静态的。我们再来一文,Lambda 设计参考简单来说,java lambda按照情况,生成的方法也不同,比如当前我们的例子,它其实是一个无状态的lambda,即当前块作用域内,就能捕获到所需要的参数,所以就能直接生成一个static的方法这里我们特地说明了块作用域,比如,下面的方法,setOnClickListener里面的lambda也依赖了一个变量a,但是他们都属于同一个块级别(函数内),void myFunc(View view){int a = 1;view.setOnClickListener(v -> {Log.e("hello","123" +a );});}复制代码生成依旧是一个static方法private static synthetic lambda$myFunc$0(ILandroid/view/View;)VL0LINENUMBER 15 L0LDC "hello"NEW java/lang/StringBuilderDUPINVOKESPECIAL java/lang/StringBuilder.<init> ()VLDC "123"INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;ILOAD 0INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)IPOP

}复制代码但是,如果我们依赖当前类的一个变量,比如类属性public String s;

void myFunc(View view){

view.setOnClickListener(v -> {
    Log.e("hello","123" +s);
});

}复制代码此时就生成一个当前类的实例方法,在当前类可以调用到该方法private synthetic lambda$myFunc$0(Landroid/view/View;)VL0LINENUMBER 15 L0LDC "hello"NEW java/lang/StringBuilderDUPINVOKESPECIAL java/lang/StringBuilder.<init> ()VLDC "123"INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;ALOAD 0GETFIELD com/example/suanfa/TestCals.s : Ljava/lang/String;INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)IPOP}复制代码同时我们也看到,这种方式会引入ALOAD 0,即this指针被捕获,因此,假如外层类与lambda生命周期不同步,就会导致内存泄漏的问题,这点需要注意噢!!同时我们也要注意,并不是所有lambda都会,像上面我们介绍的lambda就不会!kotlin lambda这里特地拿kotlin 出来,是因为它有与java层不一样的点,比如同样的代码,lambda依赖了外部类的属性,生成的方法还是一个静态的方法,而不是实例方法var s: String = 123fun test(view:View){view.setOnClickListener {Log.e("hello","$s")}}复制代码字节码如下不一样的点,选择多一个外部类的参数private final static test$lambda-0(Lcom/example/suanfa/TestKotlin;Landroid/view/View;)VL0ALOAD 0LDC "this$0"INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)VL1LINENUMBER 11 L1LDC "hello"ALOAD 0GETFIELD com/example/suanfa/TestKotlin.s : Ljava/lang/String;INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String;INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)IPOPL2LINENUMBER 12 L2RETURNL3LOCALVARIABLE this$0 Lcom/example/suanfa/TestKotlin; L0 L3 0LOCALVARIABLE it Landroid/view/View; L0 L3 1MAXSTACK = 2MAXLOCALS = 2}复制代码同样的,同一块作用域的,也当然是静态方法fun test(view:View){val s = "123"view.setOnClickListener {Log.e("hello","$s")}}复制代码如下,比起依赖了外部类的属性,没有依赖的话,自然也不用把外部类对象当作参数传入private final static test$lambda-0(Ljava/lang/String;Landroid/view/View;)VL0ALOAD 0LDC "$s"INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)VL1LINENUMBER 11 L1LDC "hello"ALOAD 0INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String;INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)IPOPL2LINENUMBER 12 L2RETURNL3LOCALVARIABLE $s Ljava/lang/String; L0 L3 0LOCALVARIABLE it Landroid/view/View; L0 L3 1MAXSTACK = 2MAXLOCALS = 2}复制代码因此,我们可以通过这两个差异,可以做一些特定的字节码逻辑。总结lambda的水还是挺深的,我们可以通过本文,去初步了解一些lambda的知识,同时我们也需要注意,在android中,也为了兼容lambda,做了一定的骚操作,比如我们常说的d8会对desuger做了一些操作等等。同时android的生成产物类,也会做单例的优化,这在一些场景会有不一样的坑,我们之后再见啦!

作者:Pika链接:https://juejin.cn/post/7206576861052420157来源:稀土掘金著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

这个估计算是一个非常有历史感的话题了,Lambda相关的文章,也有很多了,为啥还要拿出来炒炒冷饭呢?主要是最近有对Lambda的内容进行字节码处理,同时Lambda在java/kotlin/android中,都有着不一样是实现,非常有趣,因此本文算是一个记录,让我们一起去走进lambda的世界吧。当然,本文以java/kotlin视角去记录,在android中lambda的处理还不一样,我们先挖个坑,看看有没有机会填上,当然,部分的我也会夹杂的一起说!

最简单的例子

比如我们常常在写ui的时候,设置一个监听器,就是这么处理

view.setOnClickListener(v -> {
    Log.e("hello","123");
});
复制代码

编译后的字节码

 INVOKEDYNAMIC onClick()Landroid/view/View$OnClickListener; [
      // handle kind 0x6 : INVOKESTATIC
      java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
      // arguments:
      (Landroid/view/View;)V, 
      // handle kind 0x6 : INVOKESTATIC
      这里就是我们要的方法
      类名.lambda$myFunc$0(Landroid/view/View;)V, 
      (Landroid/view/View;)V
复制代码

emmm,密密麻麻,我们先不管这个,这里主要是INVOKEDYNAMIC的这个指令,这里我就不再重复INVOKEDYNAMIC的由来之类的了,我们直接来看,INVOKEDYNAMIC指令执行后的产物是啥?

生成产物类

首先产物之一,肯定是setOnClickListener里面需要的一个实现OnClickListener的对象对吧!我们都知道INVOKEVIRTUAL会在操作数栈的执行一个消耗“对象”的操作,这个从哪里来,其实也很明显,就是从INVOKEDYNAMIC执行后被放入操作数栈的。

当然,这个生成的类还是比较难找的,可以通过以下明=命令去翻翻

java -Djdk.internal.lambda.dumpProxyClasses 类路径
复制代码

当然,在AS中也有相关的生成类,在intermediates/transform目录下,不过高版本的我找不到在哪了,如果知道的朋友也可以告诉一下

调用特定方法

我们的产物类有了,但是我们也知道,lambda不是生成一个对象那么简单,而是要调用到里面的闭包方法,比如我们本例子就是

v -> {
    Log.e("hello","123");
}
复制代码

那么我们这个产物的方法在哪呢?回到INVOKEDYNAMIC指令的里面,我们看到

      java/lang/invoke/LambdaMetafactory.metafactory(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
      // arguments:
      (Landroid/view/View;)V, 
      // handle kind 0x6 : INVOKESTATIC
      类名.lambda$myFunc$0(Landroid/view/View;)V, 
      (Landroid/view/View;)V
    ]
INVOKEVIRTUAL android/view/View.setOnClickListener (Landroid/view/View$OnClickListener;)V
复制代码

这里有很多新的东西,比如LambdaMetafactory(java创建运行时类),MethodHandles等,相关概念我不赘述啦!因为有比我写的更好的文章,大家可以观看一下噢!ASM对匿名内部类、Lambda及方法引用的Hook研究

我这里特地拿出来

INVOKESTATIC 类名.lambda$myFunc$0(Landroid/view/View;)V
复制代码

这里会在生成的产物类中,直接通过INVOKESTATIC方式(当然,这里只针对我们这个例子,后面会继续有说明,不一定是通过INVOKESTATIC方式)方法是lambdamyFuncmyFuncmyFunc0,我们找下这个方法,可以看到,还真的有,如下

 private static synthetic lambda$myFunc$0(Landroid/view/View;)V
   L0
    LINENUMBER 14 L0
    LDC "hello"
    LDC "123"
    INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)I
    POP

}
复制代码

这个方法就是lambda要执行的方法,只不过在字节码中包装了一层。

至此,我们就能够大概明白了,lambda究竟干了些什么

java lambda vs Koltin lambda

java lambda

我们刚刚有提到,生成的产物方法不一定通过INVOKESTATIC的方式调用,这也间接说明了,我们的lambda的包装方法,不一定是static,即不一定是静态的。

我们再来一文,

Lambda 设计参考

简单来说,java lambda按照情况,生成的方法也不同,比如当前我们的例子,它其实是一个无状态的lambda,即当前块作用域内,就能捕获到所需要的参数,所以就能直接生成一个static的方法

这里我们特地说明了块作用域,比如,下面的方法,setOnClickListener里面的lambda也依赖了一个变量a,但是他们都属于同一个块级别(函数内),

void myFunc(View view){
    int a = 1;
    view.setOnClickListener(v -> {
        Log.e("hello","123" +a );
    });
}
复制代码

生成依旧是一个static方法

 private static synthetic lambda$myFunc$0(ILandroid/view/View;)V
   L0
    LINENUMBER 15 L0
    LDC "hello"
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "123"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ILOAD 0
    INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)I
    POP

}
复制代码

但是,如果我们依赖当前类的一个变量,比如

类属性
public String s;

void myFunc(View view){
   
    view.setOnClickListener(v -> {
        Log.e("hello","123" +s);
    });
}
复制代码

此时就生成一个当前类的实例方法,在当前类可以调用到该方法

  private synthetic lambda$myFunc$0(Landroid/view/View;)V
   L0
    LINENUMBER 15 L0
    LDC "hello"
    NEW java/lang/StringBuilder
    DUP
    INVOKESPECIAL java/lang/StringBuilder.<init> ()V
    LDC "123"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 0
    GETFIELD com/example/suanfa/TestCals.s : Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)I
    POP
}
复制代码

同时我们也看到,这种方式会引入ALOAD 0,即this指针被捕获,因此,假如外层类与lambda生命周期不同步,就会导致内存泄漏的问题,这点需要注意噢!!同时我们也要注意,并不是所有lambda都会,像上面我们介绍的lambda就不会!

kotlin lambda

这里特地拿kotlin 出来,是因为它有与java层不一样的点,比如同样的代码,lambda依赖了外部类的属性,生成的方法还是一个静态的方法,而不是实例方法

var s: String = 123
fun test(view:View){
    view.setOnClickListener {
        Log.e("hello","$s")
    }
}
复制代码

字节码如下

不一样的点,选择多一个外部类的参数
private final static test$lambda-0(Lcom/example/suanfa/TestKotlin;Landroid/view/View;)V
   L0
    ALOAD 0
    LDC "this$0"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 11 L1
    LDC "hello"
    ALOAD 0
    GETFIELD com/example/suanfa/TestKotlin.s : Ljava/lang/String;
    INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String;
    INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)I
    POP
   L2
    LINENUMBER 12 L2
    RETURN
   L3
    LOCALVARIABLE this$0 Lcom/example/suanfa/TestKotlin; L0 L3 0
    LOCALVARIABLE it Landroid/view/View; L0 L3 1
    MAXSTACK = 2
    MAXLOCALS = 2
}
复制代码

同样的,同一块作用域的,也当然是静态方法

fun test(view:View){
    val s = "123"
    view.setOnClickListener {
        Log.e("hello","$s")
    }
}
复制代码

如下,比起依赖了外部类的属性,没有依赖的话,自然也不用把外部类对象当作参数传入

  private final static test$lambda-0(Ljava/lang/String;Landroid/view/View;)V
   L0
    ALOAD 0
    LDC "$s"
    INVOKESTATIC kotlin/jvm/internal/Intrinsics.checkNotNullParameter (Ljava/lang/Object;Ljava/lang/String;)V
   L1
    LINENUMBER 11 L1
    LDC "hello"
    ALOAD 0
    INVOKESTATIC java/lang/String.valueOf (Ljava/lang/Object;)Ljava/lang/String;
    INVOKESTATIC android/util/Log.e (Ljava/lang/String;Ljava/lang/String;)I
    POP
   L2
    LINENUMBER 12 L2
    RETURN
   L3
    LOCALVARIABLE $s Ljava/lang/String; L0 L3 0
    LOCALVARIABLE it Landroid/view/View; L0 L3 1
    MAXSTACK = 2
    MAXLOCALS = 2
}
复制代码

因此,我们可以通过这两个差异,可以做一些特定的字节码逻辑。

总结

lambda的水还是挺深的,我们可以通过本文,去初步了解一些lambda的知识,同时我们也需要注意,在android中,也为了兼容lambda,做了一定的骚操作,比如我们常说的d8会对desuger做了一些操作等等。同时android的生成产物类,也会做单例的优化,这在一些场景会有不一样的坑,我们之后再见啦!

作者:Pika

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

来源:稀土掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

全部评论
有其他语言的解释吗?
点赞 回复 分享
发布于 2023-03-17 13:39 河南
怎么掘金的跑到牛客来了?
点赞 回复 分享
发布于 2023-03-17 13:51 陕西

相关推荐

不愿透露姓名的神秘牛友
10-15 14:22
点赞 评论 收藏
分享
点赞 评论 收藏
分享
评论
2
2
分享
牛客网
牛客企业服务