Swift - async/await

457 阅读5分钟

「这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战

在异步代码的上下文中管理应用程序的内存往往是一件特别棘手的事情,因为通常需要随着时间的推移捕获和保留各种对象和值,以便执行和处理我们的异步调用。

虽然 Swift 相对较新的 async/await 语法确实使许多类型的异步操作更容易编写,但在管理此类异步代码中涉及的各种任务和对象的内存时,它仍然需要我们非常小心。

Implicit captures

有趣方面是,async/await(以及我们在从同步上下文中调用它时需要用来包装此类代码的 Task 类型),当我们的异步代码正在执行时,对象和值通常最终会被隐式捕获。

例如,假设我们正在处理 DocumentViewController,它会下载并显示从给定 URL 下载的 Document。 当我们的视图控制器即将显示给用户时,为了让我们的下载延迟执行,我们在视图控制器的 viewWillAppear 方法中开始该操作,然后我们要么在可用时渲染下载的文档,要么显示任何错误 遇到过——像这样:

image.png

现在,如果我们快速查看上面的代码,它可能看起来没有任何对象捕获正在进行。毕竟,异步捕获传统上只发生在转义闭包中,这反过来又要求我们在访问此类闭包中的本地属性或方法时始终显式引用 self(即当 self 引用类实例时) .

所以我们可能期望,如果我们开始显示我们的 DocumentViewController,然后在它的下载完成之前离开它,一旦没有外部代码(例如它的父 UINavigationController)维护对它的强引用,它将被成功地释放。但实际上并非如此。

这是因为前面提到的隐式捕获会在我们创建任务时发生,或者使用 await 来等待异步调用的结果。任务中使用的任何对象都将自动保留,直到该任务完成(或失败),包括当我们引用其任何成员时的 self ,就像我们在上面所做的那样。

在许多情况下,这种行为实际上可能不是问题,并且可能不会导致任何实际的内存泄漏,因为一旦捕获任务完成,所有捕获的对象最终都会被释放。但是,假设我们希望 DocumentViewController 下载的文档可能非常大,并且如果用户在不同屏幕之间快速导航,我们不希望多个视图控制器(及其下载操作)保留在内存中。

解决这类问题的经典方法是执行弱自捕获,这通常伴随着一个守卫 let self 在捕获闭包本身内表达——为了将弱引用转换为强引用,然后可以使用在闭包的代码中:

image.png

不幸的是,这在这种情况下不起作用,因为当我们的异步 URLSession 调用被挂起时,我们的本地自引用仍将保留,直到我们所有闭包的代码完成运行(就像保留函数中的局部变量一样 直到退出该范围)。

因此,如果我们真的想要弱捕获 self,那么我们必须在整个闭包过程中始终使用弱自引用。 为了更简单地使用我们的 urlSession 和 documentURL 属性,我们可以分别捕获它们,因为这样做不会阻止我们的视图控制器本身被释放:

image.png

好消息是,有了上述内容,如果我们的视图控制器在下载完成之前最终被关闭,它现在将被成功释放。

但是,这并不意味着它的任务会自动取消。 在这种特殊情况下,这可能不是问题,但如果我们的网络调用导致某种副作用(如数据库更新),那么即使在我们的视图控制器被释放后,该代码仍会运行,这可能导致 错误或意外行为。

Cancelling tasks

一旦我们的 DocumentViewController 内存不足,确保任何正在进行的下载任务确实会被取消的一种方法是存储对该任务的引用,然后在我们的视图控制器被释放时调用它的取消方法:

image.png

现在一切都按预期工作了,我们所有的视图控制器的内存和异步状态都会在它被关闭后自动清理——但我们的代码在这个过程中也变得相当复杂。必须为每个执行异步任务的视图控制器编写所有内存管理代码会非常乏味,甚至可能让我们质疑 async/await 是否真的给我们带来了比组合、委托或闭包等技术真正的好处。

值得庆幸的是,还有另一种实现上述模式的方法,它不涉及太多代码和复杂性。由于约定是让长时间运行的异步方法在被取消时抛出错误(有关更多信息,请参阅这篇关于延迟异步任务的文章),我们可以在视图控制器即将被关闭时简单地取消 loadingTask - 这将让我们的任务抛出一个错误,退出并释放所有捕获的对象(包括自身)。这样,我们不再需要弱捕获 self,或者做任何其他类型的手动内存管理工作——给我们以下实现:

image.png