美文网首页
Kotlin lambda之-Capturing vs Non-

Kotlin lambda之-Capturing vs Non-

作者: BlueSocks | 来源:发表于2023-10-22 17:44 被阅读0次

    概述

    相信很多人看到Capturing/non-capturing lambdas的时候,很疑惑,这是什么东西,好像实际使用过程中,并没有看到这个。其实这个东西我们在实际开发中每天都碰到,只是这个概念我们不是很熟悉,今天我就来说说这个东西。了解完这个概念之后,我们

    什么是 Capturing/Non-Capturing Lambdas

    那到底什么是Capturing/non-capturing lambdas呢?从字面上可以看到Capturing lambdasnon-capturing lambdas是两个相对的概念,这里我把他直译为捕获类型的lambda非捕获类型的lambda。lambda 我们都知道,就是指的 lambda 表达式,但是这里的捕获和非捕获是啥意思呢?我看下英文解释:

    Lambdas are said to be "capturing" if they access a non-static variable or object that was defined outside of the lambda body

    从解释可以看出,如果一个 lambda 没有引用外部的非静态变量或者对象,则把这个 lambda 称为non-capturing lambdas,如果引用了则称为Capturing lambdas。所以这里根据意思,我们可以把capturing理解为引用就很好理解了。

    例子

    我先举一个non-capturing lambdas的例子

    class LambdaTest2(private val viewModel: TestViewModel, private val lifecycleOwner: LifecycleOwner) {
        fun initView() {
            viewModel.liveData.observe(lifecycleOwner) {
                println("receive data")
            }
        }
    }
    
    

    上面的给一个 监听LiveData数据的例子,使用的 lambda 就是属于non-capturing lambdas,因为内部没有引用任务外部的变量。再看一个Capturing lambdas的例子

    class LambdaTest2(private val viewModel: TestViewModel, private val lifecycleOwner: LifecycleOwner) {
        private val message: String? = null
    
        fun initView2() {
            viewModel.liveData.observe(lifecycleOwner) {
                println("receive data,toast=${message}")
            }
        }
    }
    
    

    上面这个例子中,引用了外部的 message 变量,所以这是一个Capturing lambdas

    注意:Capturing/non-capturing lambdas,这个概念并不是 kotlin 独有,他是一个语言级别的概念,只要一门语言支持 lambda,一般都有这个,比如 Java,C++等。

    实质上的区别

    那么了解了概念,他们到底有什么区别呢?引用外部变量和不引用外部变量有什么区别?要了解这个,就需要我们看下最终编译的结果是什么,我们先看一个例子:

    class LambdaTest2(private val viewModel: TestViewModel, private val lifecycleOwner: LifecycleOwner) {
        private val message: String? = null
    
        // `Capturing lambdas`,引用了外部变量
        fun initView2() {
            viewModel.liveData.observe(lifecycleOwner) {
                println("receive data,toast=${message}")
            }
        }
    
        // `non-capturing lambdas`,没有引用外部变量
        fun initView() {
            viewModel.liveData.observe(lifecycleOwner) {
                println("receive data")
            }
        }
    }
    
    

    代码很简单,就是观察 LiveData 数据变化,一个内部引用了外部变量message,一个没有引用外部变量,我看下它编译之后的代码,内部代码做了简化:

    public final class LambdaTest2 {
        private final Context context;
        private final LifecycleOwner lifecycleOwner;
        private final TestViewModel viewModel;
    
        public LambdaTest2(TestViewModel viewModel, LifecycleOwner lifecycleOwner) {
            Intrinsics.checkNotNullParameter(viewModel, "viewModel");
            Intrinsics.checkNotNullParameter(lifecycleOwner, "lifecycleOwner");
            this.viewModel = viewModel;
            this.lifecycleOwner = lifecycleOwner;
        }
    
        public final void initView2() {
            // 注释1
            this.viewModel.getLiveData().observe(this.lifecycleOwner, new LambdaTest2$sam$androidx_lifecycle_Observer(new LambdaTest2$initView2$1(this)));
        }
    
        public final void initView() {
            // 注释2
            this.viewModel.getLiveData().observe(this.lifecycleOwner, new LambdaTest2$sam$androidx_lifecycle_Observer(LambdaTest2$initView$1.INSTANCE));
        }
    }
    
    final class LambdaTest2$initView$1 extends Lambda implements Function1<String, Unit> {
        // 静态变量,只会创建一次
        public static final LambdaTest2$initView$1 INSTANCE = new LambdaTest2$initView$1();
        ......
    }
    
    

    在上面代码中:

    • 注释 1 位置,也就是initView2方法,这里是Capturing lambdas,引用了外部的变量。编译之后,可以看到,编译器自己构建了一个Observer变量LambdaTest2$sam$androidx_lifecycle_Observer,并传入了一个参数new LambdaTest2$initView2$1(this),这个对象就是我们的 lambda 中的逻辑。因为引用了外部类的变量,所以这里把外部类的对象this传递。所以这里,可以知道每次调用 observe 方法,都会把 lambda 表达式,构建一个对应的新对象。

    • 注释 2 位置,也就是initView方法,这里是 non-capturing lambdas,没有引用外部变量。编译之后,可以看到,同样编译器自己构建了一个Observer变量LambdaTest2$sam$androidx_lifecycle_Observer,但是这里不一样的地方是传递的参数是一个静态对象LambdaTest2$initView$1.INSTANCE,这个对象也是对应的 lambda 的内容

    综上比较可以知道:Capturing lambdas(捕获 lambda)会每次把调用的 lambda 的内容,创建一个对应的对象,而 non-capturing lambdas(未捕获 lambda),因为没有引用外部变量,只会创建一次对象,然后把对象当作静态变量传递进去。

    那这有什么差异呢?如果单次调用和创建,可能没有什么差异,但是如果是多次调用呢,就有差异了,比如在一个循环体中使用 lambda,就会不一样,因为如果是Capturing lambdas(捕获 lambda)就会每次创建对象,而 non-capturing lambdas(未捕获 lambda)只会创建一个,所以很明显 non-capturing lambdas(未捕获 lambda)的性能更好一些,有效的防止内存的抖动。

    所以者对我们实际开发的启示是:尽量使用non-capturing lambdas(未捕获 lambda),特别是在一些循环嵌套的情况下,这样能减少不少中间类的创建。

    特殊情况

    经过我自己的验证,发现如果在 kotlin 中调用 Java 定义的(ASM)接口时,并不会出现这种情况,比如:

        fun testForJava() {
            LinearLayout(context).apply {
                // 引用了外部类的变量
                setOnClickListener {
                    println("receive data,toast=${message}")
                }
                // 没有应用外部类的变量
                setOnClickListener {
                    println("receive data")
                }
            }
        }
    
    

    如果按照上面的分类,那么编译后,第一个 setOnClickListener 中的 lambda 会 new 一个对象,而第二个 setOnClickListener 会是一个静态变量。但事实上并不是这样,我们可以看下编译后的代码

        public final void testForJava() {
            LinearLayout $this$testForJava_u24lambda_u242 = new LinearLayout(this.context);
            // 自动创建了一个OnClickListener的匿名内部类
            $this$testForJava_u24lambda_u242.setOnClickListener(new View.OnClickListener() { // from class: com.example.effectkotlin.LambdaTest2$$ExternalSyntheticLambda0
                @Override // android.view.View.OnClickListener
                public final void onClick(View view) {
                    // 调用一个静态方法
                    LambdaTest2.testForJava$lambda$2$lambda$0(LambdaTest2.this, view);
                }
            });
            // 自动创建了一个OnClickListener的匿名内部类
            $this$testForJava_u24lambda_u242.setOnClickListener(new View.OnClickListener() { // from class: com.example.effectkotlin.LambdaTest2$$ExternalSyntheticLambda1
                @Override // android.view.View.OnClickListener
                public final void onClick(View view) {
                    // 调用一个静态方法
                    LambdaTest2.testForJava$lambda$2$lambda$1(view);
                }
            });
        }
    
        /* JADX INFO: Access modifiers changed from: private */
        public static final void testForJava$lambda$2$lambda$0(LambdaTest2 this$0, View it) {
            Intrinsics.checkNotNullParameter(this$0, "this$0");
            System.out.println((Object) ("receive data,toast=" + this$0.message));
        }
    
        /* JADX INFO: Access modifiers changed from: private */
        public static final void testForJava$lambda$2$lambda$1(View it) {
            System.out.println((Object) "receive data");
        }
    
    
    

    从上面可以看出,kotlin 针对 Java 的 ASM 接口,并没有Capturing/non-capturing lambdas的概念,都是封装为一个静态方法,如果有外部引用,就当作方法参数进行传递。这里我不太理解为什么会这样做?但是只要记住这里是有差异的就行,在使用的时候注意到这些差别。

    相关文章

      网友评论

          本文标题:Kotlin lambda之-Capturing vs Non-

          本文链接:https://www.haomeiwen.com/subject/kvqdidtx.html