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 在闭包内部被引用时。

您想要避免的典型保留循环需要两件事:

  1. 闭包保留self,而
  2. self保留闭包返回

如果 self 强烈保留闭包,并且闭包强烈保留 self,则发生保留循环 — 默认情况下,ARC 规则没有进一步的干预,两个对象都不能被释放(因为有东西保留了它),所以内存永远不会被释放。

有两种方法可以打破这个循环:

  1. 当您完成调用闭包时,在闭包和 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() 之前,您还有一个保留周期。或者,

  2. 让其中一个对象保留另一个。通常,这是通过决定哪个对象是 parent-child 关系中另一个对象的所有者来完成的,通常,self 最终强烈保留闭包,而闭包通过弱引用 self weak self,避免保留它

这些规则是正确的不管什么是self,闭包做什么:网络调用、动画回调等

使用您的原始代码,如果 apiClientself 的成员,您实际上只有一个保留周期,并且在网络请求期间保持闭包:

func performRequest() {
   apiClient.performRequest { [weak self] result in
      self?.handleResult(result)
   }
}

如果闭包实际上是在别处调度的(例如,apiClient不直接保留闭包),那么你实际上不需要[weak self],因为从来没有一个循环开始!

规则 完全 与 Swift 并发和 Task:

相同
  1. 您传递给 Task 以对其进行初始化的闭包默认保留其引用的对象(除非您使用 [weak ...]
  2. Task 在任务持续期间(即执行时)保持闭包
  3. 如果 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 对象一起使用通常没有什么意义。请改用取消模式。


一些详细的注意事项:

  1. 不需要弱捕获列表。

    你说:

    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] 不需要

  2. 尽管如此,弱捕获列表还是很有用的。

    在这些传统的转义闭包模式中使用 [weak self] 仍然有用。具体来说,在没有 weakself 的引用的情况下,闭包将保持对 self 的强引用,直到异步过程完成。

    一个常见的例子是当您发起网络请求以显示场景中的某些信息时。如果在某些异步网络请求正在进行时关闭场景,则将视图控制器保留在内存中没有意义,等待仅更新早已消失的关联视图的网络请求。

    不用说,weakself 的引用实际上只是解决方案的一部分。如果保留 self 等待异步调用的结果没有意义,那么让异步调用继续也通常没有意义。例如,我们可以将 weakself 的引用与取消挂起的异步进程的 deinit 结合起来。

  3. 弱捕获列表在 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 的隐藏强引用。

  4. 使任务可取消。

    与其担心是否应该使用 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或者其他类似的机制来添加取消支持。但是这超出了这个问题的范围。)