目录
前言
Swift 是一种非常强大的编程语言,是为 Apple 生态系统开发应用程序的首选;iOS、macOS、watchOS 和 tvOS。作为使用 Swift 编写代码的开发人员,我们经常使用闭包;语言的一个重要而重要的章节。
闭包不是初学者开始的主题。然而,这是每个人都必须尽快了解的东西。有很多方面需要了解并了解它们的工作原理。在所有这些中,有一个特定的;转义闭包和@escaping属性。在这篇文章中,我将尽可能简单地解释它们是什么以及它们可能带来的附带影响。
转义与非转义闭包
在谈论转义闭包时,我们总是指作为函数或方法参数提供的闭包。一般来说,我们将提供给方法(或函数)的闭包分为两类:
- 在方法执行完成之前调用的闭包。
- 在方法执行完成后调用的闭包。
在后一种情况下,我们谈论的是转义闭包;关闭该继续即使电子住后的的xecution方法,直到我们在以后的任何时间在未来给他们打电话。
在前一种情况下,与我上面描述的完全相反,我们称闭包为non-escaping。
直到 Swift 3,默认情况下,所有作为参数传递给方法或函数的闭包都被认为是转义的。自 Swift 3 以来,这不再正确;默认情况下,所有方法都被认为是非转义的,这意味着它们在方法执行完成之前被调用。
以下代码部分演示了一个非转义闭包:
func add(num1: Double, num2: Double, completion: (_ result: Double) -> Void) { let sum = num1 + num2 completion(sum) }
所述completion封闭件之前执行代码叶调用的方法,所以这不是一个逸出闭合的情况。
然而,一个闭包是如何从一个方法中逃脱的,所以我们最终得到了与上述情况相反的结果?
逃离方法
为了使闭包成为转义闭包,有必要将对其的引用保留在方法的范围之外,以便我们稍后使用它。看看下面的代码:
class Demo { var result: Double? var resultHandler: (() -> Void)? func add2(num1: Double, num2: Double, completion: () -> Void) { resultHandler = completion result = num1 + num2 } }
这里我们有一个result属性,它保存在方法内部发生的加法的结果。但我们也resultHandler有财产;this 保持对completion作为方法参数提供的闭包的引用。
闭包通过以下行从方法中转义:
resultHandler = completion
然而,这不是唯一需要的操作,所以我们可以说这completion是一个转义闭包。我们必须明确指出编译器,否则我们将在 Xcode 中看到以下错误:
为了修复它,我们需要用@escaping属性标记闭包。我们将此属性放在闭包名称和分号之后,但在闭包类型之前,如下所示:
func add2(num1: Double, num2: Double, completion: @escaping () -> Void) { ... }
编译器不再抱怨,completion现在正式成为转义闭包。
将转义关闭付诸行动
让我们在上面的Demo类中再添加两个方法;一个将调用add2(num1:num2:completion:)方法,另一个将调用resultHandler闭包以获得最终结果:
class Demo { ... func doubleSum(num1: Double, num2: Double) { add2(num1: num1, num2: num2) { guard let result = self.result else { return } self.result = result * 2 } } func getResult() { resultHandler?() } }
第一种方法将 add 方法计算的结果加倍。但是,该结果不会翻倍,并且在add2(num1:num2:completion:)我们调用该getResult()方法之前,不会执行该方法中闭包主体内的代码。
这是我们从转义闭包中受益的地方;我们可以在我们的代码中需要的时候以及在合适的时机触发闭包的调用。尽管提供的示例故意过于简单,但在实际项目中,实际优势会变得更加明显和大胆。
注意强参考周期
让我们为Demo类添加最后一个,并实现默认的初始化器和析构器方法:
class Demo { init() { print("Init") } deinit { print("Deinit") } ... }
init()是Demo初始化实例时调用的第一个方法,deinit也是释放实例之前调用的最后一个方法。我向它们都添加了一个打印命令,以验证它们是否被调用,并且在使用带有上述转义闭包的方法时没有内存泄漏。
注意:当我们尝试通过将对象设置为 nil 来释放它时,可能存在内存泄漏,但该对象仍保留在内存中,因为该对象与其他保持其活动状态的对象之间存在强引用。
现在,让我们添加以下几行来使用上述所有内容:
var demo: Demo? = Demo() demo?.doubleSum(num1: 5, num2: 10) demo?.getResult() print((demo?.result!)!) demo = nil
首先,我们初始化类的一个可选实例Demo,以便稍后我们可以将其设为 nil。然后,我们调用该doubleSum(num1:num2:)方法以将作为参数给出的两个数字相加,然后将该结果加倍。但是,正如我之前所说的,在我们调用该getResult()方法之前不会发生这种情况;在方法中实际调用转义闭包的那个add2(num1:num2:completion:)。
最后,我们打印实例中result属性的值demo,并将其demo设为 nil。
*注意:*如上面的代码片段所示,使用感叹号 (!) 强制展开可选值是一种非常糟糕的做法,请不要这样做。我在这里这样做的唯一原因是为了让事情尽可能简单。
以上行将打印以下内容:
Init
30.0
请注意,此处缺少“Deinit”消息!也就是说deinit没有调用该方法,证明制作demo实例nil没有实际结果。看起来,只需几行简单的代码,我们就设法解决了内存泄漏问题。
内存泄漏背后的原因
在我们找到解决内存泄漏的方法之前,有必要了解它发生的原因。为了找到它,让我们退后几步来修改我们之前所做的。
首先,我们使用以下completion行使闭包从方法中逃逸:
resultHandler = completion
这条线比看起来更“有罪”,因为它创建了对闭包的强烈引用completion。
注意:闭包是引用类型,就像类一样。
然而,仅凭这一点还不足以产生问题,因为释放demo实例会删除对闭包的引用。真正的麻烦始于doubleSum(num1:num2:)方法内部的闭包主体。
在那里,我们这次通过在使用对象访问属性时捕获**对象来创建另一个从闭包到demo实例的强引用:selfresult
guard let result = self.result else { return } self.result = result * 2
当它们都到位时,Demo 实例保持对闭包的强引用,而闭包则是对实例的强引用。这会创建一个保留循环,也称为强引用循环。发生这种情况时,每个引用类型都会使另一个引用类型在内存中保持活动状态,因此它们最终都不会被释放。
请注意,这仅发生在包含带有转义闭包的方法的类中。structs 的情况有所不同,因为它们不是引用而是值类型,并且显式引用self不是强制性的。
消除强引用循环
有两种方法可以避免强引用循环,从而避免内存泄漏。第一个是在我们调用闭包后手动且显式地释放对闭包的引用:
func getResult() { resultHandler?() // Setting nil to resultHandler removes the reference to closure. resultHandler = nil }
第二种方法是在闭包的主体中弱**捕获self实例:
func doubleSum(num1: Double, num2: Double) { add2(num1: num1, num2: num2) { [weak self] in guard let result = self?.result else { return } self?.result = result * 2 } }
[weak self] in在关闭打开后查看添加。有了这个,我们建立了对 Demo 实例的弱引用,因此我们避免了保留循环。请注意,我们将self用作可选值,并在其后加上问号 (?) 符号。
没有必要应用这两种更改以避免强引用循环。无论我们最终选择哪一个,从现在开始,输出也将包含“Deinit”消息。这意味着该demo对象变为 nil,并且我们不再有内存泄漏。
Init
30.0
Deinit
概括
离开这里需要带上一件事,那就是在使用转义闭包时要小心。无论您是实现自己的接受转义闭包作为参数的方法,还是使用具有转义闭包的 API,请始终确保不会以强引用循环结束。在开发应用程序时,内存泄漏是一个很大的“禁忌”,我们当然不希望我们的应用程序在某个时候崩溃或因此被系统终止。另一方面,不要犹豫使用转义闭包;它们提供了可以产生更强大代码的优势。