Kotlin中的 lambda 表达式
Kotlin 中 lambda 表达式的行为
在 Kotlin 中,如果一个 lambda 表达式没有使用在其代码块之外的变量,也就是不捕获外部变量,那处理起来很简单,直接创建一个该 lambda 表达式的唯一 Lambda 实例即可。
那如果 lambda 表达式内捕获了外部变量呢?就像下面这样:
1 | |
传给 process 的 lambda 表达式中使用了外部的 sum 变量,这会导致每次在不同上下文调用这个 process 函数时都重新创建一个 Lambda 实例,当在一个循环次数很大的循环体中时,会产生严重的性能损耗。
捕获过程伪代码如下:
1 | |
修改为 IntRef 的原因相信不难理解,因为 Int 类型是基本类型,如果程序员在 lambda 作用域中修改捕获变量的值,是无法反应在外部的,同理,在外部修改也无法反映在 Lambda 对象的内部,所以为了解决这个问题,会统一将基本数据类型改为 XxxRef 类实例的形式,这样 Lambda 就能持有对它的引用,外部修改和内部修改就能同步了。
为什么捕获变量的上下文变化时需要重新创建 Lambda 对象?
我们来看如下例子:
1 | |
上述代码中,#1标记处的 Lambda 表达式就属于捕获变量上下文会变化的情况,因为每一轮循环的 value 变量的值都是不一样的,所以每一轮循环都必须重新创建 Lambda 对象
如果你运行上面的代码,将会输出:
1 | |
但是如果 Kotlin 不每次都新建一个 Lambda 对象而是复用首次创建的那个对象呢?
在解释这个问题之前我们需要澄清另一个问题——假设 Kotlin 复用首次创建的 Lambda 对象,那么每次遇到相同的变量,但是该变量值改变了,Kotlin 是否应该相应地修改 Lambda 对象内的变量值?
我们这里假定 Kotlin 会进行相应的修改,因为这样才符合逻辑。
那么我们继续分析原来的问题,如果 Kotlin 一直复用首次创建的 Lambda 对象,会发生什么。
value = i = 1 时首次创建 Lambda 对象我们这里将这个对象记为$lambda,其内部有一个成员变量类型为 InfRef,其值为 1
value = i = 2 时,Kotlin 继续复用$lambda,但由于value 的值改变了,所以 Kotlin 也修改$lambda 中的成员变量值为 2
value = i = 3 时与上一种情况同理
那么这个程序最终会输出什么?答案是:
1 | |
因为被 add 到 tasks 列表中的实际上都是$lambda这同一个对象的引用,并且在最后一次循环结束时,其内部捕获的变量值为 3,所以会输出三个 3,这显然不是我们想要的结果。
那再来看看如果遇到这种情况,Kotlin 每次都新创建一个 Lambda 对象而不是复用。
value = i = 1 时,创建$lambda1,内部变量值设为 1
value = i = 2 时,创建$lambda2,内部变量值设为 2
value = i = 3 时,创建$lambda3,内部变量值设为 3
最终被添加到 tasks 的三个 Lambda 对象都是不同的三个对象,且内部变量的值依次为 1、2、3,最终输出就是 1 2 3.
思考
内存泄漏
从上面的分析可以看出,lambda 表达式一旦捕获了外部变量,那么它就会持有这个变量的引用,那么在 Android 中,如果我们在一个 Activity 中写了一个 lambda 表达式,并且其中使用了这个 Activity 的某些成员变量或方法,那么就会导致这个 Lambda 对象持有本 Activity 的引用,如果这个 Lambda 对象一直没有消除被其他对象持有的话,那么 JVM 的 GC 机制是无法回收 Lambda 对象以及 Activity 的,这就会导致 Activity 泄露,所以在 Android 中使用 lambda 表达式的时候一定要格外小心。
甚至有些时候,仅仅只一个日志打印,就可能造成 Activity 泄露,来看一个例子:
1 | |
TaskManager是一个全局单例,它持有#1 标记处的 Lambda 对象,而这个 Lambda 对象由于内部使用了 TAG,这个 TAG 是 Activity 的成员变量,所以 Lambda 也会持有这个 Activity 对象,只要 TaskManager.task 仍然是这个 Lambda 对象,那么这个 Activity 就永远不会被回收。
解决办法也很简单,把 TAG 改成静态变量即可:
1 | |
或者把TAG改成顶级变量,MyActivity.kt:
1 | |
这样 Lambda 对象就不会持有 Activity 的引用了。
性能优化
在循环体内,若 lambda 表达式捕获外部变量,将导致多个 Lambda 对象被创建,很影响性能
为了避免这种情况,我们可以使用 inline 关键字标记接受 lambda 表达式的函数,例如:
1 | |
用 inline 标记后的函数,不仅会把函数本身的内容内联到调用处,还会把函数接收的 lambda 表达式直接展开在调用处,上述代码等价于:
1 | |
这样既没有 Lambda 对象的创建,也没有函数调用的开销,性能得到了提升