kotlin的lambda和java匿名内部类的区别

2,788 阅读4分钟

之前笔者一直以为lambda是匿名内部类的语法糖,但无意间反编译了apk之后发现:

Kotlin的lambda和Java的匿名内部类是有区别的。

由此写下这篇博客,作为一个踩坑的记录。

先说结论:

  • Java的匿名内部类无论是否需要捕获外部实例,都会在生成的类中捕获外部类的引用

  • 而Kotlin的lambda则是

    • 对于捕获lambda表达式,每次将lambda作为参数传递时,都将创建一个新函数实例,执行完毕后被当做垃圾回收;

    • 对于非捕获lambda表达式(纯函数),将会创建一个单例的函数实例以便以后复用。

(当Lambda表达式访问一个定义在表达式体外的非静态变量或者对象时,这个Lambda表达式称为“捕获的”。)

ps:下面的图片是apk反编译之后的Smali文件。Smali是Android虚拟机的反汇编语言。处于性能的考虑,Android平台并没有使用标准的JVM,而是使用专门的Anroid虚拟机(5.0以下为Dalvik,5.0以上为ART)。Android虚拟机的可执行文件并不是普通的class文件,而是再重新整合打包后生成的dex文件。dex文件反编译之后就是Smali代码,所以,Smali语法是Android虚拟机的反汇编语言。

口说无凭,我们就来实际写一个lambda和匿名内部类反编译来看看到底内部是什么样子

示例1:Kotlin中的lambda:

我们使用Kotlin语言写一个lambda

按照结论,由于示例中的调用者代码使用的是非捕获lambda表达式的形式(调用Thread.sleep方法,并没有需要用到外部的Activity的实例),因此lambda被编译为单例,而不是内部类。

d12221a2-5ffb-458c-9981-6c9caaac9c39 (1)

可以看到第12行实际上被编译成了三个指令:

首先,我们知道sget-object用于获取静态变量,这里使用sget-object指令取得Lcom/example/kotlinlambdatest/MainActivity$onCreate$1这个类的INSTANCE单例,存放到v0这个寄存器中;

这一点说明实际执行过程中,我们并没有构造了一个lambda所在类的对象出来,而是使用了一个单例的形式。

然后,check-cast指令用于将v0寄存器中的对象转成Runnable类型

最后,invoke-virtual指令,调用类的test()方法

57cb18b7-55b7-4e90-842a-5509a080fb80

示例2:kotlin中的匿名内部类

如果不需要,则不会去捕获外部类的实例:

  • 以下示例在onCreate()方法中,调用test()方法传入了一个实现了Runnable接口的匿名内部类,第二张图片是查看生成的smali。

可以看到kotlin中的匿名内部类和上面的lambda有所不同:

首先,调用new-instance,构造了Lcom/exmple/kotlinlambdatest/MainActivity$onCreate$1的实例;

然后,调用了这个类的<init>方法,也就是构造方法;

然后,调用check-cast指令,转型为Runnable;

最后,invoke-virtual指令,调用了Activity的test方法;

示例3:Java中的lambda:

可以看到Java中的lambda则是:

首先,.local指令,指定了本地的v0寄存器存储了Lcom/example/kotlinlambdatest/JavaLambdaTest这类的实例;

然后,sget-object指v1寄存器保存了一个生成的INSTANCE单例

最后,invoke-virtual调用了类的test方法;

示例4:Java中的匿名内部类

无论如何都会捕获外部类的实例:

  • 以下示例在onCreate()方法中,调用test()方法传入了一个实现了Runnable接口的匿名内部类,第二张图片是查看生成的smali。可以看到第33行传入了调用了SecondActivity$1这个类的<init>()方法,传入了外部类的实例

我用Android Profiler测试了一下,当从SecondActivity回退到MainActivity的时候,发生了内存泄漏:

总结

这篇博客主要是列举了kotlin的lambda,kotlin的匿名内部类,java的lambda以及java的匿名内部类这四者之间的区别。

  • kotlin的lambda和java的lambda一样,如果不需要捕获外部类的实例,则实际生成的是一个单例;

  • kotlin的匿名内部类则是,如果不需要捕获外部类的实例,在生成对象的时候,调用构造方法不会传入外部类的实例;

  • java的匿名内部类则是,不管是否需要捕获外部类的实例,都会在生成对象的时候,调用构造方法传入外部类的实例;