簡體   English   中英

如何解決Swift 4中的內存循環URLSession.downloadTask?

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

我有一個基本的viewController ,只有一個按鈕,當點擊時,調用一個方法開始從給定的有效URL下載圖像。

我非常小心地將任何強大的指針傳遞給viewcontroller。 而且我可以將視圖控制器解散掉並且回到演示控制器而沒有任何問題。 但是,即使調用了viewcontroller的deinit ,也不會釋放由viewcontroller創建的Web()對象實例。

我究竟做錯了什么?

BTW:啟動文件下載,報告進度並報告文件位置。 但是,一旦文件下載完畢,該對象永遠不會被釋放。

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類 - 下載文件方法)中找出問題的原因:

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相關的問題沒有擺脫它; 因此,您必須執行-kind of-manual處理來解決它,調用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類中的deinit中添加斷點,你應該看到它按預期工作(委托方法可以工作,然后調用deinit )。


此外:

如果您想了解有關如何使用Memory Graph Debugger的更多信息,可以檢查: 當Leaks儀器沒有顯示時,如何調試內存泄漏?

使用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 無效來打破強引用。 但總的來說,簡單的解決方案是:首先不要創建圓形。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM