处理 URLSession 时总是需要 [Weak self] 吗?

iOS*_*eek 4 closures ios retain-cycle nsurlsession swift

我不知道我是否需要[weak self]在这种情况下使用?

HTTPClient.swift:

struct HTTPClient {
    let session = URLSession.shared

    func get(url: URL, completion: @escaping (Data) -> Void) {
        session.dataTask(with: url) { data, urlResponse, error in
        completion(data) // assume everything will go well
      }.resume()
    }
}
Run Code Online (Sandbox Code Playgroud)

服务.swift

struct Service {
    let httpClient: HTTPClient

    init(httpClient: HTTPClient = HTTPClient()) {
        self.httpClient = httpClient
    }

    func fetchUser(completion: @escaping (User) -> Void) {
        httpClient.get("urlToGetUser") { data in
          // transform data to User 
          completion(user)
        } 
    }
}
Run Code Online (Sandbox Code Playgroud)

视图模型.swift

class ViewModel {
   let service: Service
   let user: User?
   var didLoadData: ((User) -> Void)?

    init(service: Service) {
        self.service = service
        loadUser()
    }

   func loadUser() {
     service.fetchUser { [weak self] user in // is  [weak self] really needed ?
            self?.user = user
            self?.didLoadData?(user)
        }
   }
}
Run Code Online (Sandbox Code Playgroud)

用户真的需要这里[weak self]吗?当我们处理一个我们不知道闭包发生了什么的 API 时,是否有关于如何检查它是否需要的规则?或者这无关紧要(由我们决定)?

Rob*_*ier 8

在您给出的示例中,[weak self]可能是不必要的。如果ViewModel在请求完成之前被释放,这取决于您想要发生的情况。

URLSessionDataTask文档中所述(强调我的):

创建任务后,您可以通过调用它的 resume() 方法来启动它。然后会话保持对任务的强引用,直到请求完成或失败;你不需要维护对任务的引用,除非它对你的应用程序的内部簿记很有用。

会话对任务有很强的引用。任务对闭包有很强的引用。闭包对ViewModel. 只要ViewModel没有对任务的强引用(它在您提供的代码中没有),那么就没有循环。

问题是您是否要确保ViewModel继续存在足够长的时间来执行闭包。如果您这样做(或不关心),那么您可以使用简单的强引用。如果您想阻止任务保持ViewModel活动状态,那么您应该使用弱引用。

这就是您需要考虑参考周期的方式。没有一般规则“weak在这里使用”。weak当这就是你的意思时,你使用; 当你不希望这个闭包一直self存在直到它被释放时。如果它创建一个循环,则尤其如此。但是对于“这是否会产生循环”并没有统一的答案。这取决于哪些部分包含参考。

这也指出了您当前的 API 设计不如预期的好。你传递didLoadDatainit。这很可能会创建引用循环并强制您的调用者使用weak. 相反,如果您在 上创建didLoadDdata了一个完成处理程序loadUser(),那么您可以避免该问题并使调用者的生活更轻松。

func loadUser(completion: @escaping ((User?) -> Void)? = nil) {
    service.fetchUser {
        self?.user = user
        didLoadData?(user)
    }
}
Run Code Online (Sandbox Code Playgroud)

(在任何情况下,您当前的 API 都存在竞争条件。您可以设置loadUser()之前开始didLoadData。您可能假设完成处理程序在您设置之前不会完成dataDidLoad,但没有真正的承诺。这可能是真的,但它充其量是脆弱的。)

  • 您如何识别“URLSessionDataTask”保留闭包的引用?作为一个苹果 API,我们看不到它的内部结构,文档似乎也没有提到这一点。 (2认同)
  • @DeclanMcKenna 签名告诉我们这一点,因为闭包在 Swift 中被标记为“@escaping”。完成处理程序总是由它们传递给的对象保留,或者它们不能是完成处理程序,因此在 ObjC 中我们按照惯例知道它,但在 Swift 中签名还告诉我们闭包何时逃逸。 (2认同)
  • 这是否意味着每当“@escaping”闭包 1. 引用“self” 2. 被传递到属于“self”属性的对象的函数时,我们都会有保留循环。当我在操场上运行这样的场景时,至少在涉及 URLSession 的情况下似乎并非如此:https://gist.github.com/Deco354/716cd67ae0f733b5cff0b330e9ab8f18 (2认同)
  • 在完成处理程序被释放之前,您确实有一个保留周期。这是通过“会话保持对任务的强引用直到请求完成或失败”来解释的。一旦任务完成,它就会被会话释放。这会破坏任务及其所持有的闭包,从而将引用释放回自身,从而破坏保留周期。对象在触发后释放其完成块也是非常常见和良好的做法,但这里没有明确记录,只是任务将被释放。 (2认同)