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

lol*_*f64 28 lambda android memory-leaks kotlin

在阅读了关于内存泄漏的这篇文章之后,我想知道在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()
}
Run Code Online (Sandbox Code Playgroud)

我的问题很简单:为正负按钮设置的两个lambdas可以导致内存泄漏吗?(我的意思是,kotlin lambdas只是转换为Java匿名函数吗?)

编辑:也许我在这个Jetbrains话题中得到了答案.

Sta*_*ots 27

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

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

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

因此它与Java的行为类似,除了内联案例甚至更便宜.这种编码lambdas的有效方法是Kotlin中的函数式编程比Java更具吸引力的一个原因.


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


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

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

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

它们在函数体中访问变量意味着什么有点令人困惑.是否还计算了对封闭类实例的引用?

我已经看到了你在问题中引用的主题,但它似乎已经过时了.我发现多了最新信息在这里:

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

所以,不幸的是,似乎Kotlin的lambdas与Java的Anonymous Inner Classes有同样的问题.

为什么匿名内部类很糟糕?

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();
   }
}
Run Code Online (Sandbox Code Playgroud)

如您所见,在这种情况下,在执行长时间运行的任务之前会出现内存泄漏.一种解决方法是使用静态嵌套类.

由于Kotlin's 非内联 lambda拥有对封闭类实例的引用,因此它们具有与内存泄漏类似的问题.

奖励:与其他Lambda实现的快速比较

Java 8 Lambdas

句法:

内存泄漏问题:规范中所述:

对此的引用 - 包括通过非限定字段引用或方法调用的隐式引用 - 基本上是对最终局部变量的引用.包含此类引用的Lambda主体捕获相应的实例.在其他情况下,对象不保留对此的引用.

简单地说,如果你不使用封闭类中的任何字段/方法,就没有隐式引用,this就像匿名类一样.

Retrolambda

来自文档

Lambda表达式通过将它们转换为匿名内部类来向后移植.这包括优化对无状态lambda表达式使用单例实例以避免重复的对象分配.

我想,这是不言自明的.

Apple的Swift

句法:

  • 声明类似于Kotlin,在Swift lambdas中称为闭包:

    func someFunctionThatTakesAClosure(closure: (String) -> Void) {}
    
    Run Code Online (Sandbox Code Playgroud)
  • 通过关闭

    someFunctionThatTakesAClosure { print($0) }
    
    Run Code Online (Sandbox Code Playgroud)

    在这里,$0请参考闭包的第一个String参数.这与itKotlin 相对应.注:与科特林,在斯威夫特我们也可以参考其他参数一样$1,$2等等.

内存泄漏问题:

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

开发人员也可以轻松指定他们只想捕获弱引用:

   someFunctionThatTakesAClosure { [weak self] in print($0) }
Run Code Online (Sandbox Code Playgroud)

我希望在Kotlin也有可能:)

  • 确实,我的意思是匿名类。是的,Kotlin应该确实为lambda实施了弱引用捕获选项,这是真的同意的。因此,我明白了这一点,对于Kotlin lambda,我需要比对Java内部/匿名类更加谨慎。所以Kotlin Lambda是不安全的,我应该使用静态内部类,并对其中的任何View引用使用WeakReferences。 (3认同)

aga*_*aga 10

当因为不再需要而应该删除的某个对象无法删除时会发生内存泄漏,因为具有较长生命周期的对象具有对此对象的引用.最简单的例子是存储参考Activitystatic变量(我从Java的角度讲,但它在科特林类似):用户点击"返回"按钮后Activity不需要了,但将保持但是在内存中 - 因为某些静态变量仍然指向此活动.
现在,在您的示例中,您没有将自己分配Activity给某个static变量,没有object涉及Kotlin 可以防止您Activity被垃圾收集 - 您的代码中涉及的所有对象的生命周期大致相同,这意味着没有内存泄漏.

PS我已经刷新了Kotlin对lambda的实现的记忆:在负按钮单击处理程序的情况下,你没有引用外部作用域,因此编译器将创建单击的侦听器实例,它将被重用于所有点击此按钮.对于正向按钮单击侦听器,您将引用外部作用域(this@MainActivity),因此在这种情况下,Kotlin将在每次创建对话框时创建匿名类的新实例(此实例将引用外部类,MainActivity),所以行为与在Java中编写此代码完全相同.

  • 好吧,它与Java中的基本相同:当对话框的单击侦听器执行某些工作时(假设此单击侦听器使用某些活动方法),活动是否会被垃圾收集?否。点击侦听器执行职责并完成活动后,是否会对其进行垃圾回收?是。 (2认同)

Gra*_*rks 10

这是一个(潜在的)泄漏的简单示例,其中闭包/块捕获this

class SomeClass {
    fun doWork() {
        doWorkAsync { onResult() } // leaks, this.onResult() captures `this`
    }

    fun onResult() { /* some work */ }
}
Run Code Online (Sandbox Code Playgroud)

您需要使用 Wea​​kReference。

fun doWork() {
    val weakThis = WeakReference(this)
    doWorkAsync { weakThis?.get()?.onResult() } // no capture, no leak!
}
Run Code Online (Sandbox Code Playgroud)

如果 Kotlin 从 Swift 复制 [弱自我] 语法糖的想法,那就太好了。