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

时间:2017-02-16 10:27:31

标签: android lambda memory-leaks kotlin

阅读this article about Memory Leaks之后,我想知道在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()
}

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

编辑:也许我已经得到了答案in this Jetbrains Topic

3 个答案:

答案 0 :(得分:23)

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

  

与Java一样,Kotlin的情况因情况而异。

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

因此,除了内联案例外,它与Java的行为相似   它哪里更便宜。这种编码lambdas的有效方法   是Kotlin中函数式编程更具吸引力的一个原因   而不是在Java。

修改(2017年2月17日):我已在Kotlin discussions中发布了有关此主题的问题。也许Kotlin工程师会带来一些新的东西。

  

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

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

  

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

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

我已经看到了您在问题中引用的主题,但它似乎已经过时了。我找到了更多最新信息here

  

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

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

为什么匿名内部类不好?

来自Java specs

  

类O的直接内部类C的实例i是关联的   使用O的实例,称为直接封闭的实例   一世。对象的直接封闭实例(如果有)是   确定何时创建对象

这意味着匿名类将始终对封闭类的实例具有隐式引用。由于引用是隐含的,因此无法摆脱它。

看一下简单的例子

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..."));
    

内存泄漏问题:如specs中所述:

  

对此的引用 - 包括隐式引用   不合格的字段引用或方法调用 - 是,   基本上,对最终局部变量的引用。 Lambda的身体   包含此类引用捕获适当的此实例。的在   在其他情况下,对象不会保留对此的引用

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

Retrolambda

来自docs

  

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

我想,这是不言自明的。

Apple的Swift

语法:

  • 宣言类似于Kotlin,在Swift lambdas中称为闭包:

    func someFunctionThatTakesAClosure(closure: (String) -> Void) {}
    
  • 传递闭包

    someFunctionThatTakesAClosure { print($0) }
    

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

内存泄漏问题:

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

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

self.someMethod()

我希望Kotlin也有可能:)

答案 1 :(得分:10)

当因为不再需要而应该删除的某个对象无法删除时会发生内存泄漏,因为具有较长生命周期的对象具有对此对象的引用。最简单的示例是将Activity的引用存储在static变量中(我从Java角度讲,但它在Kotlin中类似):用户点击后on' Back'按钮Activity不再需要,但它仍会保留在内存中 - 因为某些静态变量仍然指向此活动。
现在,在您的示例中,您没有将Activity分配给某个static变量,而且没有任何Kotlin的object可以保留您的Activity this@MainActivity垃圾收集 - 代码中涉及的所有对象的生命周期大致相同,这意味着不会有内存泄漏。

P.S。我已经在Kotlin的lambdas实现中刷新了我的记忆:在负按钮点击处理程序的情况下,你没有引用外部范围,因此编译器将创建单击监听器的单个实例这将在该按钮的所有点击中重复使用。对于正向按钮单击侦听器,您引用外部作用域(MainActivity),因此在这种情况下,Kotlin将在每次创建对话框时创建匿名类的新实例(并且这个实例将引用外部类{{1}}),因此行为与您用Java编写此代码完全相同。

答案 2 :(得分:0)

class bar {
    fun work() {
        somethingAsyncTakingALambda { foo() } // leaks, this.foo() captures this
    }

    fun foo() { println("foo") }
}

您需要使用WeakReference。

fun work() {
    val weakThis = WeakReference(this)
    somethingAsyncTakingALambda { this?.get()?.foo() }
}

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