Swift4URLSession.downloadTask内存循环如何解决?

How to resolve memory cycle in Swift 4 URLSession.downloadTask?

我有一个基本的 viewController,只有一个按钮,当点击该按钮时,它会调用一个方法开始从给定的有效 url.

下载图像

我非常小心地将任何强指针传递给 viewcontroller。而且我能够关闭 viewcontroller 并毫无问题地返回到呈现控制器。然而,由 viewcontroller 创建的 Web() 的对象实例不会被释放,即使 viewcontroller 的 deinit 被调用。

我做错了什么?

顺便说一句:文件下载开始,报告进度并报告文件位置。但是,一旦文件下载完成,该对象就永远不会被释放。

viewController.swift

@IBAction func buttonTapped(_ sender: UIButton) {
   //first create an instance of Web class (download helper)
   let web = Web(url: "https://www.google.com.sa/images/branding/googlelogo/2x/googlelogo_color_120x44dp.png")

   // all call back closures have no reference to self    
   web.downloadFile(
       finishedHandler: {fileLocation in print("file stored to \(fileLocation)")},
       progressHandler: {bytes,total in print("written \(bytes) out of \(total)")},
       errorHandler: {msg in print("error: \(msg)")}
  )

}

并且我已将 Web() 定义为 NSObject,它符合 URLSessionDownloadDelegate 以利用委托事件方法(didFinishDownload / didWriteBytes 等) .

Web.swift

import UIKit

class Web : NSObject {
    var urlToDownload : String?

   // the following variables are references to closures passed to the object.
    var progressCallback : ((_ bytesWritten:Int64, _ totalExpectedBytes: Int64)->())?
    var finishedCallback : ((_ fileLocation: String)->())?


    static var instanceCount = 0 // keep track of number of instances created

    init(url: String) {

       Web.instanceCount += 1
       urlToDownload = url
       print(" new instance of Web created. Total : \(Web.instanceCount)")
   }

   deinit {
      Web.instanceCount -= 1
      print("Web instance deallocated. Remaining: \(Web.instanceCount)")

   }
}

并且我将文件下载逻辑添加为同一文件的扩展:

extension Web : URLSessionDownloadDelegate {

    func downloadFile(
        finishedHandler: @escaping (_ fileLocation:String)->(),
        progressHandler: @escaping (_ bytesWritten:Int64, _ totalBytes: Int64)->(),
        errorHandler: @escaping (_ errorMsg:String)->()) {

        // we need to capture the closure because, these will
        // be called once the delegate methods are triggered
        self.progressCallback = progressHandler
        self.finishedCallback = finishedHandler


        if let url = URL(string: self.urlToDownload!) {
             let session = URLSession(
                configuration: .default,
                delegate: self,
                delegateQueue: nil)

            let task = session.downloadTask(with: url)

            task.resume()

        }

    }

    // MARK :- Delegate methods
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
        // call the closure if it still exists
        self.finishedCallback?(location.absoluteString)
    }

    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
        // report progress by calling captured closure, if exists
        self.progressCallback?(totalBytesWritten,totalBytesExpectedToWrite)
    }
}

在我追踪问题之后(使用 Memory Graph Debugger),我发现问题的原因在这段代码中(Web class - 下载文件方法):

if let url = URL(string: self.urlToDownload!) {
     let session = URLSession(
        configuration: .default,
        delegate: self,
        delegateQueue: nil)

    let task = session.downloadTask(with: url)

    task.resume()
}

实际上有一个与实例化相关的问题URLSession没有摆脱它;因此,你必须做一种 - 手动处理来解决它,调用 finishTasksAndInvalidate() 适合这样的问题:

if let url = URL(string: self.urlToDownload!) {
    let session = URLSession(
        configuration: .default,
        delegate: self,
        delegateQueue: nil)

    let task = session.downloadTask(with: url)

    task.resume()
    // here we go:
    session.finishTasksAndInvalidate()
}

为了确保它按预期工作,我建议在 Web class 中的委托方法和 deinit 添加断点,您应该会看到它按预期工作(委托方法会工作,然后 deinit 会被调用)。


此外:

如果您想了解更多关于如何使用 Memory Graph Debugger 的信息,您可以查看:

使用 Memory Graph Debugger 进行调试时您可以清楚地看到的问题是对象 WebURLSession 对象持有CFXURLCache 反过来阻止了 URLSession。他们彼此有很强的参考。因此只有当会话或缓存从内存中释放时,Web 对象才会从内存中释放。


解决方案很简单,您需要在下载完成后使会话无效以打破NSURLSessionCFXURLCache之间的循环引用:

func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
    // call the closure if it still exists
    self.finishedCallback?(location.absoluteString)
    session.finishTasksAndInvalidate()
}

问题在于 URLSession 保留了它的委托,因此在充当其 delegate 的同一对象中存储对 URLSession 的强引用是循环的。

如您所知,您可以通过使 URLSession 失效来破坏强引用。但总的来说,简单的解决方案是:首先不要创建循环。