async/await、Task 和 [weak self]

Mat*_*ney 33 concurrency ios async-await swift swift-concurrency

好的,我们都知道,在 Swift 的传统并发中,如果您在类中执行(例如)网络请求,并且在完成该请求时您引用了属于该类的函数,则必须传入[weak self],例如这:

func performRequest() {
   apiClient.performRequest { [weak self] result in
      self?.handleResult(result)
   }
}
Run Code Online (Sandbox Code Playgroud)

这是为了阻止我们self在闭包中强烈捕获并导致不必要的保留/无意中引用已经从内存中删除的其他实体。

在异步/等待中怎么样?我在网上看到了相互矛盾的事情,所以我只想向社区发布两个示例,看看您对这两个示例有何看法:

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")
   }
}

Run Code Online (Sandbox Code Playgroud)

function3很简单 - 老式并发意味着使用[weak self]. function2我认为是对的,因为我们仍在闭包中捕获内容,所以我们应该使用[weak self]. function1这是由 Swift 处理的,还是我应该在这里做一些特别的事情?

Rob*_*Rob 61

[weak self]最重要的是,将捕获列表与对象一起使用通常没有什么意义Task。请改用取消模式。

\n
\n

一些详细的考虑:

\n
    \n
  1. 不需要弱捕获列表。

    \n

    你说:

    \n
    \n

    在 Swift 的传统并发中,如果您在类内执行(例如)网络请求,并且在完成该请求时引用属于该类的函数,则必须传递[weak self]\xe2\x80\xa6

    \n
    \n

    这不是真的。是的,使用捕获列表可能是谨慎或明智的做法[weak self],但这不是必需的。weak\xe2\x80\x9cm 必须\xe2\x80\x9d 使用引用的唯一时间self是当存在持久的强引用循环时。

    \n

    对于编写良好的异步模式(被调用的例程在完成后立即释放闭包),不存在持续的强引用循环风险。这[weak self]不是必需

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

    \n

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

    \n

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

    \n

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

    \n
  4. \n
  5. 弱捕获列表在 Swift 并发中用处不大。

    \n

    考虑你的这种排列function2

    \n
    func function2() {\n    Task { [weak self] in\n        let result = await apiClient.performNetworkRequestAsync()\n        \xe2\x80\xa6\n    }\n}\n
    Run Code Online (Sandbox Code Playgroud)\n

    这看起来不应该对selfwhile performNetworkRequestAsyncis in Progress 保持强引用。但是对属性 的引用apiClient将引入强引用,而不会出现任何警告或错误消息。例如,在下面,我AsyncClass在红色路标处让其超出了范围,但尽管有[weak self]捕获列表,但直到异步过程完成后它才被释放:

    \n

    在此输入图像描述

    \n

    在这种情况下,捕获[weak self]列表的作用很小。请记住,在 Swift 并发中,幕后发生了很多事情(例如, \xe2\x80\x9c 挂起点 \xe2\x80\x9d 之后的代码是 \xe2\x80\x9ccontinuation\xe2\x80\x9d 等。 )。它与简单的 GCD 调度不同。请参阅Swift 并发:幕后花絮

    \n

    但是,如果您weak也进行所有属性引用,那么它将按预期工作:

    \n
    func function2() {\n    Task { [weak self] in\n        let result = await self?.apiClient.performNetworkRequestAsync()\n        \xe2\x80\xa6\n    }\n}\n
    Run Code Online (Sandbox Code Playgroud)\n

    希望未来的编译器版本能够警告我们这种隐藏的对self.

    \n
  6. \n
  7. 使任务可取消。

    \n

    与其担心是否应该使用weak对 的引用self,不如考虑简单地支持取消:

    \n
    var task2: Task<Void, Never>?\n\nfunc function2() {\n    task2?.cancel()        // you might want to cancel previous task, if any; it depends\xe2\x80\xa6\n\n    task2 = Task {\n        let result = await apiClient.performNetworkRequestAsync()\n        \xe2\x80\xa6\n        task2 = nil\n    }\n}\n
    Run Code Online (Sandbox Code Playgroud)\n

    进而:

    \n
    override func viewDidDisappear(_ animated: Bool) {\n    task2?.cancel()\n    \xe2\x80\xa6\n    super.viewDidDisappear(animated)\n}\n
    Run Code Online (Sandbox Code Playgroud)\n

    显然,这是假设您的任务支持取消。大多数 Apple 异步 API 都是如此。(但是,如果您编写了自己的withUnsafeContinuation- 样式实现,那么您将需要定期检查Task.isCancelled或将您的调用包装在withTaskCancellationHandler或其他类似的机制中以添加取消支持。但这超出了本问题的范围。)

    \n

    现在,详细信息将根据您的具体要求而有所不同。例如,上面的模式仅跟踪Task每个函数的一个。如果您可能多次调用它,那么您将需要弄清楚如何处理它。您是否希望始终取消前一项任务(如上所示),以便始终只有一项任务需要跟踪?或者您想要跟踪多个任务,以便在视图关闭时可以取消所有任务。它有所不同。而且,SwiftUI 通常甚至需要完全不同的模式(例如,如果在.task\xc2\xa0{\xe2\x80\xa6}视图修饰符中启动任务,那么当视图关闭时它会自动取消)。这完全取决于您的具体要求。

    \n

    但不要迷失在这些细节中:总体思路是在不再需要任务时取消任务。这就消除了对任务保持强引用时间self超过所需时间的担忧。

    \n
  8. \n
\n

  • “希望未来的编译器版本能够警告我们这种隐藏的对 self 的强引用。” Xcode 13.4 中的编译器似乎将此视为警告,但在 Swift 6 中它将成为错误。 (6认同)

Ita*_*ber 16

\n

如果您正在类内执行(例如)网络请求,并且在完成该请求时您引用了属于该类的函数,则必须传递 [weak self],如下所示

\n
\n

这不完全正确。当您在 Swift 中创建闭包时,默认情况下会保留闭包引用或“关闭”的变量,以确保在调用闭包时这些对象可以有效使用。这包括self, whenself在闭包内部被引用。

\n

您想要避免的典型保留周期需要满足两件事:

\n
    \n
  1. 闭包保留self, 和
  2. \n
  3. self保留封盖
  4. \n
\n

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

\n

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

\n
    \n
  1. self显式地断开闭包与调用完闭包之间的链接,例如 ifself.action是一个引用 的闭包self,一旦被调用就赋值nil给,例如self.action

    \n
    self.action = { /* Strongly retaining `self`! */\n    self.doSomething()\n\n    // Explicitly break up the cycle.\n    self.action = nil\n}\n
    Run Code Online (Sandbox Code Playgroud)\n

    这通常不适用,因为它是一次性的self.action,并且您还有一个保留周期,直到您调用self.action(). 或者,

    \n
  2. \n
  3. 让其中一个对象保留另一个对象。通常,这是通过确定父子关系中哪个对象是另一个对象的所有者来完成的,并且通常最终会强保留闭包,而通过弱引用self闭包,以避免保留它selfweak self

    \n
  4. \n
\n

这些规则都是正确的,无论闭包是什么self,也不管它做什么:是否是网络调用、动画回调等。

\n

apiClient使用原始代码,实际上只有 if是 的成员才具有保留周期self,并且在网络请求期间保留闭包:

\n
func performRequest() {\n   apiClient.performRequest { [weak self] result in\n      self?.handleResult(result)\n   }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

如果闭包实际上被分派到其他地方(例如,apiClient不直接保留闭包),那么您实际上不需要[weak self],因为从一开始就没有循环!

\n

规则与 Swift 并发完全相同Task

\n
    \n
  1. 您传递给 aTask来初始化它的闭包默认保留它引用的对象(除非您使用[weak ...]
  2. \n
  3. Task在任务期间(即执行时)保持闭包
  4. \n
  5. 如果在执行期间self保留,您将有一个保留周期Task
  6. \n
\n

在 的情况下function2()Task会异步启动和分派,但self 不会保留结果Task对象,这意味着不需要[weak self]。相反,如果function2()存储创建的Task那么您将有一个潜在的保留周期,您需要将其分解:

\n
class AsyncClass {\n    var runningTask: Task?\n\n    func function4() {\n        // We retain `runningTask` by default.\n        runningTask = Task {\n            // Oops, the closure retains `self`!\n            self.printSomething()\n        }\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

如果您需要保留该任务(例如,这样您就可以完成cancel它),您将希望避免让该任务保留self回来(Task { [weak self] ... })。

\n