Kotlin:安全的lambdas(没有内存泄漏)?

在阅读了关于内存泄漏的这篇文章之后,我想知道在Kotlin Android项目中使用lambdas是否安全。 确实,lambda语法使得我的程序更加轻松,但是内存泄漏呢?

作为一个有问题的例子,我从我的一个项目中取得了一段代码,在那里构建了一个AlertDialog。 这段代码在我的项目的MainActivity类中。

fun deleteItemOnConfirmation(id: Long) : Unit { val item = explorerAdapter.getItemAt(id.toInt()) val stringId = if (item.isDirectory) R.string.about_to_delete_folder else R.string.about_to_delete_file val dialog = AlertDialog.Builder(this). setMessage(String.format(getString(stringId), item.name)).setPositiveButton( R.string.ok, {dialog: DialogInterface, id: Int -> val success = if (item.isDirectory) ExplorerFileManager.deleteFolderRecursively(item.name) else ExplorerFileManager.deleteFile(item.name) if (success) { explorerAdapter.deleteItem(item) explorerRecyclerView.invalidate() } else Toast.makeText(this@MainActivity, R.string.file_deletion_error, Toast.LENGTH_SHORT).show() }).setNegativeButton( R.string.cancel, {dialog: DialogInterface, id: Int -> dialog.cancel() }) dialog.show() } 

我的问题很简单:两个lambda表达式设置为正面和负面的按钮会导致内存泄漏? (我也是说,kotlin lambda简单地转换为Java匿名函数?)

编辑:也许我已经得到了我在这个Jetbrains主题的答案。

编辑(2017年2月19日):我收到了Mike Hearn关于这个问题的非常全面的回复 :

就像在Java中一样,Kotlin中发生的情况在不同情况下会有所不同。

  • 如果lambda被传递给一个内联函数并且没有标记为noinline,那么整个事情就会消失,并且不会创建额外的类或对象。
  • 如果lambda没有捕获,那么它将作为一个单例类被重复使用(一个类+一个对象分配)。
  • 如果lambda捕获,那么每次使用lambda时都会创建一个新的对象。

因此,除了内联更便宜的情况之外,它与Java类似。 这种编码lambda的有效方法是Kotlin中的函数式编程比Java更有吸引力的一个原因。


编辑(2017年2月17日):我在Kotlin讨论中发布了关于这个主题的问题 。 也许Kotlin的工程师会带来一些新的东西。


是kotlin lambdas简单地转换为Java匿名函数?

我自己在问这个问题(这里只是一个简单的更正:这些被称为匿名类 ,而不是函数)。 Koltin文件没有明确的答案。 他们只是说明

使用高阶函数会施加一定的运行时间惩罚:每个函数都是一个对象,它捕获一个闭包,即在函数体中访问的那些变量。

在函数的主体中访问变量的含义有点令人困惑。 封闭类的实例的引用是否也被计算在内?

我已经在你的问题中看到了你正在引用的主题,但是现在看来它已经过时了。 我在这里找到了更多的最新信息:

Lambda表达式或匿名函数保留封闭类的隐式引用

所以,不幸的是,似乎Kotlin的lambdas和Java的匿名内部类有相同的问题。

为什么匿名内部类是不好的?

Java 规范 :

类O的直接内部类C的实例i与O的实例相关联,被称为i的立即封闭实例。 对象的立即封闭实例(如果有的话)在创建对象时确定

这意味着匿名类将始终隐式引用封闭类的实例。 而且由于这个引用是隐含的,所以没有办法摆脱它。

看看这个微不足道的例子

 public class YourActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); new Thread(new Runnable() { // the inner class will keep the implicit reference to the outer activity @Override public void run() { // long-running task } }).start(); } } 

正如你所看到的,在这种情况下,在执行长时间运行的任务之前,会有内存泄漏。 一个解决方法是使用静态嵌套类。

由于Kotlin's 非内联 lambdas持有对封闭类的实例的引用,因此它们在内存泄漏方面有类似的问题。

奖金:与其他Lambda实现进行快速比较

Java 8 Lambdas

句法:

  • 声明SAM(单抽象方法)接口

     interface Runnable { void run(); } 
  • 将此接口用作lambda的类型

     public void canTakeLambda(Runnable r) { ... } 
  • 通过你的lambda

     canTakeLambda(() -> System.out.println("Do work in lambda...")); 

内存泄漏问题:如规格中所述 :

对此的引用(包括通过非限定字段引用或方法调用的隐式引用)本质上是对最终局部变量的引用。 包含这些引用的Lambda正文捕获了相应的实例。 在其他情况下,对象不保留对此的引用

简而言之,如果不使用封闭类中的任何字段/方法,则不会像匿名类那样隐式引用this

Retrolambda

从文档

通过将Lambda表达式转换为匿名内部类来进行backport。 这包括使用无状态lambda表达式的单例实例来优化,以避免重复的对象分配。

我想,这是不言自明的。

苹果的Swift

句法:

  • 声明与Kotlin类似,Swift中的lambda被称为闭包:

     func someFunctionThatTakesAClosure(closure: (String) -> Void) {} 
  • 通过关闭

     someFunctionThatTakesAClosure { print($0) } 

    在这里, $0引用闭包的第一个String参数。 这对应于Kotlin。 注意:与Kotlin不同的是,在Swift中,我们也可以引用其他参数,如$1$2等。

内存泄漏问题:

在Swift中,就像在Java 8中一样,只有当它访问实例的属性(如self.someProperty ,或者如果闭包调用实例上的方法,闭包才会捕获对self (在Java和Kotlin中)的强引用,比如self.someMethod()

开发人员也可以很容易地指定他们只想捕获弱引用:

  someFunctionThatTakesAClosure { [weak self] in print($0) } 

我希望Kotlin也有可能:)

内存泄漏发生时,应删除,因为它不再需要了一些对象不能被删除,因为有一个更长的生命周期有一个对这个对象的引用。 最简单的例子是将对Activity的引用存储在static变量中(我从Java的角度来讲,但是在Kotlin中它是相似的):在用户点击“返回”按钮之后,不再需要该Activity ,但是它将会保留在内存中 – 因为有些静态变量仍然指向这个活动。
现在,在你的例子中,你并没有将Activity分配给某个static变量,也没有涉及Kotlin的object ,这可以让你的Activity不被垃圾回收 – 代码中涉及的所有对象的寿命大致相同,这意味着将不会有内存泄漏。

PS我已经刷新了我对Kotlin实现lambda表达式的记忆:在负面按钮点击处理程序的情况下,你不引用外部范围,因此编译器将创建单一的监听器实例,将重用所有点击这个按钮。 在肯定的按钮点击监听器的情况下,你引用的是外部范围( this@MainActivity ),所以在这种情况下,Kotlin会在每次创建一个对话框时创建一个匿名类的实例(并且这个实例将会对外部类MainActivity的引用),因此行为与您在Java中编写此代码的行为完全相同。