Kotlin:安全的 lambdas(没有内存泄漏)?
Posted
技术标签:
【中文标题】Kotlin:安全的 lambdas(没有内存泄漏)?【英文标题】:Kotlin : safe lambdas (no memory leak)? 【发布时间】: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/… 非常感谢。很快就会听他们的:) 其实没有这么有用的事实。无论如何谢谢你 【参考方案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 的匿名内部类存在相同的问题。
为什么匿名内部类不好?
来自Java
specs:
类 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 是不安全的,我应该使用静态内部类并使用 WeakReferences 对它们内部的任何 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 的 object
s 可以防止您的 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 */
您需要使用 WeakReference。
fun doWork()
val weakThis = WeakReference(this)
doWorkAsync weakThis?.get()?.onResult() // no capture, no leak!
如果 Kotlin 从 Swift 复制 [weak self] 语法糖的想法,那就太好了。
【讨论】:
以上是关于Kotlin:安全的 lambdas(没有内存泄漏)?的主要内容,如果未能解决你的问题,请参考以下文章
Android中用于kotlin的静态等价物,以避免处理程序内存泄漏
From Java To Kotlin:空安全扩展函数Lambda很详细,这次终于懂了
按值将 shared_ptr 传递给 lambda 会泄漏内存
From Java To Kotlin:空安全扩展函数Lambda很详细,这次终于懂了
Kotlin函数 ⑦ ( 内联函数 | Lambda 表达式弊端 | “ 内联 “ 机制避免内存开销 - 将使用 Lambda 表达式作为参数的函数定义为内联函数 | 内联函数本质 - 宏替换 )