async/await、任务和[弱势自我]
async/await, Task and [weak self]
好吧,我们都知道在 Swift 中的传统并发中,如果您在 class 中执行(例如)网络请求,并且在该请求完成时您引用了一个属于那个class的函数,你必须传入[weak self]
,像这样:
func performRequest() {
apiClient.performRequest { [weak self] result in
self?.handleResult(result)
}
}
这是为了阻止我们在闭包中强烈捕获 self
并导致不必要的 retention/inadvertently 引用其他已经从内存中删除的实体。
async/await怎么样?我在网上看到了相互矛盾的事情,所以我打算 post 向社区举两个例子,看看你们对这两个例子有何看法:
class AsyncClass {
func function1() async {
let result = await performNetworkRequestAsync()
self.printSomething()
}
func function2() {
Task { [weak self] in
let result = await performNetworkRequestAsync()
self?.printSomething()
}
}
func function3() {
apiClient.performRequest { [weak self] result in
self?.printSomething()
}
}
func printSomething() {
print("Something")
}
}
function3
很简单——老式的并发意味着使用 [weak self]
。
function2
我认为是对的,因为我们仍然在闭包中捕获东西,所以我们应该使用 [weak self]
。
function1
这只是由 Swift 处理,还是我应该在这里做一些特别的事情?
if you are performing (for example) a network request inside a class, and in the completion of that request you reference a function that belongs to that class, you must pass [weak self] in, like this
这不完全正确。当您在 Swift 中创建闭包时,闭包引用或“关闭”的变量默认保留,以确保在调用闭包时这些对象是有效的。这包括 self
,当 self
在闭包内部被引用时。
您想要避免的典型保留循环需要两件事:
- 闭包保留
self
,而
self
保留闭包返回
如果 self
强烈保留闭包,并且闭包强烈保留 self
,则发生保留循环 — 默认情况下,ARC 规则没有进一步的干预,两个对象都不能被释放(因为有东西保留了它),所以内存永远不会被释放。
有两种方法可以打破这个循环:
当您完成调用闭包时,在闭包和 self
之间显式中断 link,例如如果 self.action
是引用 self
的闭包,则在调用后将 nil
分配给 self.action
,例如
self.action = { /* Strongly retaining `self`! */
self.doSomething()
// Explicitly break up the cycle.
self.action = nil
}
这通常不适用,因为它使 self.action
one-shot,并且在您调用 self.action()
之前,您还有一个保留周期。或者,
让其中一个对象不保留另一个。通常,这是通过决定哪个对象是 parent-child 关系中另一个对象的所有者来完成的,通常,self
最终强烈保留闭包,而闭包通过弱引用 self
weak self
,避免保留它
这些规则是正确的不管什么是self
,闭包做什么:网络调用、动画回调等
使用您的原始代码,如果 apiClient
是 self
的成员,您实际上只有一个保留周期,并且在网络请求期间保持闭包:
func performRequest() {
apiClient.performRequest { [weak self] result in
self?.handleResult(result)
}
}
如果闭包实际上是在别处调度的(例如,apiClient
不直接保留闭包),那么你实际上不需要[weak self]
,因为从来没有一个循环开始!
规则 完全 与 Swift 并发和 Task
:
相同
- 您传递给
Task
以对其进行初始化的闭包默认保留其引用的对象(除非您使用 [weak ...]
)
Task
在任务持续期间(即执行时)保持闭包
- 如果
self
在执行期间保持 Task
,您将有一个保留周期
在 function2()
的情况下,Task
被异步启动和调度,但 self
不会保持 到结果 Task
对象,这意味着不需要 [weak self]
。相反,如果 function2()
存储了创建的 Task
, 那么 您将有一个潜在的保留周期,您需要将其分解:
class AsyncClass {
var runningTask: Task?
func function4() {
// We retain `runningTask` by default.
runningTask = Task {
// Oops, the closure retains `self`!
self.printSomething()
}
}
}
如果您需要保留任务(例如,这样您就可以 cancel
它),您将希望避免让任务保留 self
回来 (Task { [weak self] ... }
) .
最重要的是,将 [weak self]
捕获列表与 Task
对象一起使用通常没有什么意义。请改用取消模式。
一些详细的注意事项:
不需要弱捕获列表。
你说:
in traditional concurrency in Swift, if you are performing (for example) a network request inside a class, and in the completion of that request you reference a function that belongs to that class, you must pass [weak self]
…
这不是真的。是的,使用 [weak self]
捕获列表可能是谨慎的或可取的,但这不是必需的。唯一“必须”使用 weak
引用 self
的情况是存在持续的强引用循环时。
对于 well-written 异步模式(被调用的例程在完成后立即释放闭包),不存在持久的强引用循环风险。 [weak self]
不需要。
尽管如此,弱捕获列表还是很有用的。
在这些传统的转义闭包模式中使用 [weak self]
仍然有用。具体来说,在没有 weak
对 self
的引用的情况下,闭包将保持对 self
的强引用,直到异步过程完成。
一个常见的例子是当您发起网络请求以显示场景中的某些信息时。如果在某些异步网络请求正在进行时关闭场景,则将视图控制器保留在内存中没有意义,等待仅更新早已消失的关联视图的网络请求。
不用说,weak
对 self
的引用实际上只是解决方案的一部分。如果保留 self
等待异步调用的结果没有意义,那么让异步调用继续也通常没有意义。例如,我们可以将 weak
对 self
的引用与取消挂起的异步进程的 deinit
结合起来。
弱捕获列表在 Swift 并发中用处不大。
考虑一下 function2
:
的这种排列
func function2() {
Task { [weak self] in
let result = await apiClient.performNetworkRequestAsync()
self?.printSomething()
}
}
这看起来不应该在 performNetworkRequestAsync
进行时保留对 self
的强引用。但是对属性、apiClient
的引用会引入强引用,没有任何警告或错误信息。例如,在下面,我让 AsyncClass
超出了红色路标处的范围,但是尽管有 [weak self]
捕获列表,它直到异步过程完成后才被释放:
在这种情况下,[weak self]
捕获列表的作用很小。请记住,在 Swift 并发中,幕后发生了很多事情(例如,“暂停点”之后的代码是“延续”等)。它与简单的 GCD 调度不同。参见 Swift concurrency: Behind the scenes。
但是,如果您也将所有 属性 引用 weak
,那么它将按预期工作:
func function2() {
Task { [weak self] in
let result = await self?.apiClient.performNetworkRequestAsync()
self?.printSomething()
}
}
希望未来的编译器版本会警告我们这种对 self
的隐藏强引用。
使任务可取消。
与其担心是否应该使用 weak
引用 self
,不如考虑简单地支持取消:
var task: Task<Void, Never>?
func function2() {
task = Task {
let result = await apiClient.performNetworkRequestAsync()
printSomething()
task = nil
}
}
然后,
@IBAction func didTapDismiss(_ sender: Any) {
task?.cancel()
dismiss(animated: true)
}
现在,显然,假设您的任务支持取消。大多数 Apple async API 都可以。 (但是如果你自己写了withUnsafeContinuation
-style implementation, then you will want to periodically check Task.isCancelled
or wrap your call in a withTaskCancellationHandler
或者其他类似的机制来添加取消支持。但是这超出了这个问题的范围。)
好吧,我们都知道在 Swift 中的传统并发中,如果您在 class 中执行(例如)网络请求,并且在该请求完成时您引用了一个属于那个class的函数,你必须传入[weak self]
,像这样:
func performRequest() {
apiClient.performRequest { [weak self] result in
self?.handleResult(result)
}
}
这是为了阻止我们在闭包中强烈捕获 self
并导致不必要的 retention/inadvertently 引用其他已经从内存中删除的实体。
async/await怎么样?我在网上看到了相互矛盾的事情,所以我打算 post 向社区举两个例子,看看你们对这两个例子有何看法:
class AsyncClass {
func function1() async {
let result = await performNetworkRequestAsync()
self.printSomething()
}
func function2() {
Task { [weak self] in
let result = await performNetworkRequestAsync()
self?.printSomething()
}
}
func function3() {
apiClient.performRequest { [weak self] result in
self?.printSomething()
}
}
func printSomething() {
print("Something")
}
}
function3
很简单——老式的并发意味着使用 [weak self]
。
function2
我认为是对的,因为我们仍然在闭包中捕获东西,所以我们应该使用 [weak self]
。
function1
这只是由 Swift 处理,还是我应该在这里做一些特别的事情?
if you are performing (for example) a network request inside a class, and in the completion of that request you reference a function that belongs to that class, you must pass [weak self] in, like this
这不完全正确。当您在 Swift 中创建闭包时,闭包引用或“关闭”的变量默认保留,以确保在调用闭包时这些对象是有效的。这包括 self
,当 self
在闭包内部被引用时。
您想要避免的典型保留循环需要两件事:
- 闭包保留
self
,而 self
保留闭包返回
如果 self
强烈保留闭包,并且闭包强烈保留 self
,则发生保留循环 — 默认情况下,ARC 规则没有进一步的干预,两个对象都不能被释放(因为有东西保留了它),所以内存永远不会被释放。
有两种方法可以打破这个循环:
当您完成调用闭包时,在闭包和
self
之间显式中断 link,例如如果self.action
是引用self
的闭包,则在调用后将nil
分配给self.action
,例如self.action = { /* Strongly retaining `self`! */ self.doSomething() // Explicitly break up the cycle. self.action = nil }
这通常不适用,因为它使
self.action
one-shot,并且在您调用self.action()
之前,您还有一个保留周期。或者,让其中一个对象不保留另一个。通常,这是通过决定哪个对象是 parent-child 关系中另一个对象的所有者来完成的,通常,
self
最终强烈保留闭包,而闭包通过弱引用self
weak self
,避免保留它
这些规则是正确的不管什么是self
,闭包做什么:网络调用、动画回调等
使用您的原始代码,如果 apiClient
是 self
的成员,您实际上只有一个保留周期,并且在网络请求期间保持闭包:
func performRequest() {
apiClient.performRequest { [weak self] result in
self?.handleResult(result)
}
}
如果闭包实际上是在别处调度的(例如,apiClient
不直接保留闭包),那么你实际上不需要[weak self]
,因为从来没有一个循环开始!
规则 完全 与 Swift 并发和 Task
:
- 您传递给
Task
以对其进行初始化的闭包默认保留其引用的对象(除非您使用[weak ...]
) Task
在任务持续期间(即执行时)保持闭包- 如果
self
在执行期间保持Task
,您将有一个保留周期
在 function2()
的情况下,Task
被异步启动和调度,但 self
不会保持 到结果 Task
对象,这意味着不需要 [weak self]
。相反,如果 function2()
存储了创建的 Task
, 那么 您将有一个潜在的保留周期,您需要将其分解:
class AsyncClass {
var runningTask: Task?
func function4() {
// We retain `runningTask` by default.
runningTask = Task {
// Oops, the closure retains `self`!
self.printSomething()
}
}
}
如果您需要保留任务(例如,这样您就可以 cancel
它),您将希望避免让任务保留 self
回来 (Task { [weak self] ... }
) .
最重要的是,将 [weak self]
捕获列表与 Task
对象一起使用通常没有什么意义。请改用取消模式。
一些详细的注意事项:
不需要弱捕获列表。
你说:
in traditional concurrency in Swift, if you are performing (for example) a network request inside a class, and in the completion of that request you reference a function that belongs to that class, you must pass
[weak self]
…这不是真的。是的,使用
[weak self]
捕获列表可能是谨慎的或可取的,但这不是必需的。唯一“必须”使用weak
引用self
的情况是存在持续的强引用循环时。对于 well-written 异步模式(被调用的例程在完成后立即释放闭包),不存在持久的强引用循环风险。
[weak self]
不需要。尽管如此,弱捕获列表还是很有用的。
在这些传统的转义闭包模式中使用
[weak self]
仍然有用。具体来说,在没有weak
对self
的引用的情况下,闭包将保持对self
的强引用,直到异步过程完成。一个常见的例子是当您发起网络请求以显示场景中的某些信息时。如果在某些异步网络请求正在进行时关闭场景,则将视图控制器保留在内存中没有意义,等待仅更新早已消失的关联视图的网络请求。
不用说,
weak
对self
的引用实际上只是解决方案的一部分。如果保留self
等待异步调用的结果没有意义,那么让异步调用继续也通常没有意义。例如,我们可以将weak
对self
的引用与取消挂起的异步进程的deinit
结合起来。弱捕获列表在 Swift 并发中用处不大。
考虑一下
的这种排列function2
:func function2() { Task { [weak self] in let result = await apiClient.performNetworkRequestAsync() self?.printSomething() } }
这看起来不应该在
performNetworkRequestAsync
进行时保留对self
的强引用。但是对属性、apiClient
的引用会引入强引用,没有任何警告或错误信息。例如,在下面,我让AsyncClass
超出了红色路标处的范围,但是尽管有[weak self]
捕获列表,它直到异步过程完成后才被释放:在这种情况下,
[weak self]
捕获列表的作用很小。请记住,在 Swift 并发中,幕后发生了很多事情(例如,“暂停点”之后的代码是“延续”等)。它与简单的 GCD 调度不同。参见 Swift concurrency: Behind the scenes。但是,如果您也将所有 属性 引用
weak
,那么它将按预期工作:func function2() { Task { [weak self] in let result = await self?.apiClient.performNetworkRequestAsync() self?.printSomething() } }
希望未来的编译器版本会警告我们这种对
self
的隐藏强引用。使任务可取消。
与其担心是否应该使用
weak
引用self
,不如考虑简单地支持取消:var task: Task<Void, Never>? func function2() { task = Task { let result = await apiClient.performNetworkRequestAsync() printSomething() task = nil } }
然后,
@IBAction func didTapDismiss(_ sender: Any) { task?.cancel() dismiss(animated: true) }
现在,显然,假设您的任务支持取消。大多数 Apple async API 都可以。 (但是如果你自己写了
withUnsafeContinuation
-style implementation, then you will want to periodically checkTask.isCancelled
or wrap your call in awithTaskCancellationHandler
或者其他类似的机制来添加取消支持。但是这超出了这个问题的范围。)