Kotlin lambda之-Capturing vs Non-
概述
相信很多人看到Capturing/non-capturing lambdas
的时候,很疑惑,这是什么东西,好像实际使用过程中,并没有看到这个。其实这个东西我们在实际开发中每天都碰到,只是这个概念我们不是很熟悉,今天我就来说说这个东西。了解完这个概念之后,我们
什么是 Capturing/Non-Capturing Lambdas
那到底什么是Capturing/non-capturing lambdas
呢?从字面上可以看到Capturing lambdas
和non-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
的概念,都是封装为一个静态方法,如果有外部引用,就当作方法参数进行传递。这里我不太理解为什么会这样做?但是只要记住这里是有差异的就行,在使用的时候注意到这些差别。