之前笔者一直以为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被编译为单例,而不是内部类。
可以看到第12行实际上被编译成了三个指令:
首先,我们知道sget-object
用于获取静态变量,这里使用sget-object
指令取得Lcom/example/kotlinlambdatest/MainActivity$onCreate$1
这个类的INSTANCE单例,存放到v0这个寄存器中;
这一点说明实际执行过程中,我们并没有构造了一个lambda所在类的对象出来,而是使用了一个单例的形式。
然后,check-cast
指令用于将v0寄存器中的对象转成Runnable
类型
最后,invoke-virtual
指令,调用类的test()
方法
示例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的匿名内部类则是,不管是否需要捕获外部类的实例,都会在生成对象的时候,调用构造方法传入外部类的实例;