概述

相信很多人看到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的例子

1
2
3
4
5
6
7
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的例子

1
2
3
4
5
6
7
8
9
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++等。

实质上的区别

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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,一个没有引用外部变量,我看下它编译之后的代码,内部代码做了简化:

 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 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)接口时,并不会出现这种情况,比如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
    fun testForJava() {
        LinearLayout(context).apply {
            // 引用了外部类的变量
            setOnClickListener {
                println("receive data,toast=${message}")
            }
            // 没有应用外部类的变量
            setOnClickListener {
                println("receive data")
            }
        }
    }

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

 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
    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的概念,都是封装为一个静态方法,如果有外部引用,就当作方法参数进行传递。这里我不太理解为什么会这样做?但是只要记住这里是有差异的就行,在使用的时候注意到这些差别。