【问题标题】:Kotlin : safe lambdas (no memory leak)?Kotlin:安全的 lambdas(没有内存泄漏)?
【发布时间】:2017-07-05 09:39:29
【问题描述】:

在阅读了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

【问题讨论】:

  • 那么当 lambdas 不使用封闭对象的任何方法或字段时,它是否会捕获封闭对象?它们是不同的点:discuss.kotlinlang.org/t/…discuss.kotlinlang.org/t/…
  • 非常感谢。很快就会听他们的:)
  • 其实没有这么有用的事实。无论如何谢谢你

标签: android lambda memory-leaks kotlin


【解决方案1】:

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

与 Java 一样,Kotlin 中发生的事情因情况而异。

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

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


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


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

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

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

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

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

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

因此,不幸的是,Kotlin 的 lambda 似乎与 Java 的匿名内部类存在相同的问题。

为什么匿名内部类不好?

来自Javaspecs

类 O 的直接内部类 C 的实例 i 关联 有一个 O 的实例,称为 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 非内联 lambda 持有对封闭类实例的引用,因此它们在内存泄漏方面存在类似问题。

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

Java 8 Lambda

语法:

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

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

    public void canTakeLambda(Runnable r) { ... }
    
  • 传递你的 lambda

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

内存泄漏问题:specs 所述:

对此的引用——包括通过 不合格的字段引用或方法调用——是, 本质上,是对最终局部变量的引用。 Lambda 机构 包含此类引用捕获此的适当实例。 在 在其他情况下,对象不会保留对 this 的引用

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

Retrolambda

来自docs

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

我猜,这是不言自明的。

苹果的 Swift

语法:

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

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

    someFunctionThatTakesAClosure { print($0) }
    

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

内存泄漏问题:

在 Swift 中,就像在 Java 8 中一样,闭包仅在访问实例的属性(例如 self.someProperty)或如果闭包调用实例上的方法,例如self.someMethod()

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

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

我希望在 Kotlin 中也有可能:)

【讨论】:

  • 确实我的意思是匿名类。是的,Kotlin 真的应该为 lambda 实现弱引用捕获选项,真的同意这一点。所以我明白了这一点,我需要像使用 Java 内部/匿名类一样小心使用 Kotlin lambda。所以 kotlin lambdas 是不安全的,我应该使用静态内部类并使用 Wea​​kReferences 对它们内部的任何 View 引用。
  • WeakReferences 也有一些问题,而且不如内存泄漏那么明显。另外我的猜测是,它可以在 Kotlin 中重新实现你想要的,这样你的 lambdas 就可以使用weakReference(在crossinline 的帮助下)
  • 这很难理解。因此,当 lambda 捕获时,它会创建新对象,并且它们可能包含对 lambda 中使用的变量的引用。这些对象是否还包含对外部类的 this 的引用,如果 this 从未在 lambda 中使用,或者仅在使用 this 时使用?
  • 也许我只是因为习惯了 Swift 的行为而感到困惑,但我不觉得 Mike Hearn 的回答完全回答了这个问题。在 Swift 中,self 总是被捕获,除非它被声明为 [weak self]。在 Java 中(来自您的链接),“不从封闭实例捕获成员的 lambdas 不持有对它的引用”。 Mike Hearn 暗示 Kotlin 的行为类似于 Java,但并不完全清楚。 “如果 lambda 没有捕获”仍然有些模棱两可。什么决定了 lambda 是否会捕获?
  • @StanMots 您提到“只有当您在 lambda 的主体内使用此对象的属性和/或调用方法时,lambda 才会捕获对此的引用”。你能分享任何具体记录这种行为的东西吗?
【解决方案2】:

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

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

【讨论】:

  • 谢谢。您确认了我在“分析”应用程序时得到的印象:我没有注意到任何内存问题。只是不时调用 GC 的事实。
  • 但是,作为我的另一个困惑点,假设在正面按钮回调中,我可以添加(没有编译错误)以下行 explorerRecyclerView.invalidate(),其中 explorerRecyclerView 是我的 MainActivity 类的成员, lambda的间接宿主。那么,垃圾收集活动可以被阻止,对吧?
  • 嗯,这和Java中的基本一样:当Dialog的点击监听器在做一些工作(假设这个点击监听器使用了一些activity方法)时,activity会被垃圾回收吗?否。一旦点击侦听器完成其职责并且活动完成后,它会被垃圾收集吗?是的。
  • @aga 是因为android视图系统的单线程(主线程)架构。
【解决方案3】:

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

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

    fun onResult() { /* some work */ }
}

您需要使用 Wea​​kReference。

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-04-30
    • 2011-12-05
    • 2018-02-14
    • 2018-10-11
    • 2011-12-07
    • 2016-08-29
    • 2018-12-30
    • 1970-01-01
    相关资源
    最近更新 更多