How To Download Multiple Files Sequentially using NSURLSession downloadTask in Swift

匿名 (未验证) 提交于 2019-12-03 03:04:01

问题:

I have an app that has to download multiple large files. I want it to download each file one by one sequentially instead of concurrently. When it runs concurrently the app gets overloaded and crashes.

So. Im trying to wrap a downloadTaskWithURL inside a NSBlockOperation and then setting the maxConcurrentOperationCount = 1 on the queue. I wrote this code below but it didnt work since both files get downloaded concurrently.

import UIKit  class ViewController: UIViewController, NSURLSessionDelegate, NSURLSessionDownloadDelegate {      override func viewDidLoad() {         super.viewDidLoad()         // Do any additional setup after loading the view, typically from a nib.         processURLs()             }      func download(url: NSURL){         let sessionConfiguration = NSURLSessionConfiguration.defaultSessionConfiguration()         let session = NSURLSession(configuration: sessionConfiguration, delegate: self, delegateQueue: nil)         let downloadTask = session.downloadTaskWithURL(url)         downloadTask.resume()     }      func processURLs(){          //setup queue and set max conncurrent to 1         var queue = NSOperationQueue()         queue.name = "Download queue"         queue.maxConcurrentOperationCount = 1          let url = NSURL(string: "http://azspeastus.blob.core.windows.net/azurespeed/100MB.bin?sv=2014-02-14&sr=b&sig=%2FZNzdvvzwYO%2BQUbrLBQTalz%2F8zByvrUWD%2BDfLmkpZuQ%3D&se=2015-09-01T01%3A48%3A51Z&sp=r")         let url2 = NSURL(string: "http://azspwestus.blob.core.windows.net/azurespeed/100MB.bin?sv=2014-02-14&sr=b&sig=ufnzd4x9h1FKmLsODfnbiszXd4EyMDUJgWhj48QfQ9A%3D&se=2015-09-01T01%3A48%3A51Z&sp=r")          let urls = [url, url2]         for url in urls {             let operation = NSBlockOperation { () -> Void in                 println("starting download")                 self.download(url!)             }              queue.addOperation(operation)                     }     }     override func didReceiveMemoryWarning() {         super.didReceiveMemoryWarning()         // Dispose of any resources that can be recreated.     }      func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didFinishDownloadingToURL location: NSURL) {         //code     }      func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {         //     }      func URLSession(session: NSURLSession, downloadTask: NSURLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {         var progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)         println(progress)     }  }

How can write this properly to achieve my goal of only download one file at a time.

回答1:

Your code won't work because NSURLSessionDownloadTask runs asynchronously. Thus the NSBlockOperation completes before the download is done and therefore while the operations fire off sequentially, the download tasks will continue asynchronously and in parallel.

To address this, you can wrap the requests in asynchronous NSOperation subclass. See Configuring Operations for Concurrent Execution in the Concurrency Programming Guide for more information.

But before I illustrate how to do this in your situation (the delegate-based NSURLSession), let me first show you the simpler solution when using the completion handler rendition. We'll later build upon this for your more complicated question. So, in Swift 3:

class DownloadOperation : AsynchronousOperation {     var task: URLSessionTask!      init(session: URLSession, url: URL) {         super.init()          task = session.downloadTask(with: url) { temporaryURL, response, error in             defer { self.completeOperation() }              guard error == nil && temporaryURL != nil else {                 print("\(error)")                 return             }              do {                 let manager = FileManager.default                 let destinationURL = try manager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)                     .appendingPathComponent(url.lastPathComponent)                 _ = try? manager.removeItem(at: destinationURL)                    // remove the old one, if any                 try manager.moveItem(at: temporaryURL!, to: destinationURL)    // move new one there             } catch let moveError {                 print("\(moveError)")             }         }     }      override func cancel() {         task.cancel()         super.cancel()     }      override func main() {         task.resume()     }  }  /// Asynchronous operation base class /// /// This is abstract to class performs all of the necessary KVN of `isFinished` and /// `isExecuting` for a concurrent `Operation` subclass. You can subclass this and /// implement asynchronous operations. All you must do is: /// /// - override `main()` with the tasks that initiate the asynchronous task; /// /// - call `completeOperation()` function when the asynchronous task is done; /// /// - optionally, periodically check `self.cancelled` status, performing any clean-up ///   necessary and then ensuring that `completeOperation()` is called; or ///   override `cancel` method, calling `super.cancel()` and then cleaning-up ///   and ensuring `completeOperation()` is called.  public class AsynchronousOperation : Operation {      override public var isAsynchronous: Bool { return true }      private let stateLock = NSLock()      private var _executing: Bool = false     override private(set) public var isExecuting: Bool {         get {             return stateLock.withCriticalScope { _executing }         }         set {             willChangeValue(forKey: "isExecuting")             stateLock.withCriticalScope { _executing = newValue }             didChangeValue(forKey: "isExecuting")         }     }      private var _finished: Bool = false     override private(set) public var isFinished: Bool {         get {             return stateLock.withCriticalScope { _finished }         }         set {             willChangeValue(forKey: "isFinished")             stateLock.withCriticalScope { _finished = newValue }             didChangeValue(forKey: "isFinished")         }     }      /// Complete the operation     ///     /// This will result in the appropriate KVN of isFinished and isExecuting      public func completeOperation() {         if isExecuting {             isExecuting = false         }          if !isFinished {             isFinished = true         }     }      override public func start() {         if isCancelled {             isFinished = true             return         }          isExecuting = true          main()     } }  /*     Copyright (C) 2015 Apple Inc. All Rights Reserved.     See LICENSE.txt for this sample’s licensing information      Abstract:     An extension to `NSLock` to simplify executing critical code.      From Advanced NSOperations sample code in WWDC 2015 https://developer.apple.com/videos/play/wwdc2015/226/     From https://developer.apple.com/sample-code/wwdc/2015/downloads/Advanced-NSOperations.zip */  extension NSLock {      /// Perform closure within lock.     ///     /// An extension to `NSLock` to simplify executing critical code.     ///     /// - parameter block: The closure to be performed.      func withCriticalScope(block: () -> T) -> T {         lock()         let value = block()         unlock()         return value     } }

Or in Swift 2:

/// Asynchronous NSOperation subclass for downloading  class DownloadOperation : AsynchronousOperation {     var task: NSURLSessionTask!      init(session: NSURLSession, URL: NSURL) {         super.init()          task = session.downloadTaskWithURL(URL) { temporaryURL, response, error in             defer {                 self.completeOperation()             }              print(URL.lastPathComponent)              guard error == nil && temporaryURL != nil else {                 print(error)                 return             }              do {                 let manager = NSFileManager.defaultManager()                 let documents = try manager.URLForDirectory(.DocumentDirectory, inDomain: .UserDomainMask, appropriateForURL: nil, create: false)                 let destinationURL = documents.URLByAppendingPathComponent(URL.lastPathComponent!)                 if manager.fileExistsAtPath(destinationURL.path!) {                     try manager.removeItemAtURL(destinationURL)                 }                 try manager.moveItemAtURL(temporaryURL!, toURL: destinationURL)             } catch let moveError {                 print(moveError)             }         }     }      override func cancel() {         task.cancel()         super.cancel()     }      override func main() {         task.resume()     }  }   // //  AsynchronousOperation.swift // //  Created by Robert Ryan on 9/20/14. //  Copyright (c) 2014 Robert Ryan. All rights reserved. //  import Foundation  /// Asynchronous Operation base class /// /// This class performs all of the necessary KVN of `isFinished` and /// `isExecuting` for a concurrent `NSOperation` subclass. So, to developer /// a concurrent NSOperation subclass, you instead subclass this class which: /// /// - must override `main()` with the tasks that initiate the asynchronous task; /// /// - must call `completeOperation()` function when the asynchronous task is done; /// /// - optionally, periodically check `self.cancelled` status, performing any clean-up ///   necessary and then ensuring that `completeOperation()` is called; or ///   override `cancel` method, calling `super.cancel()` and then cleaning-up ///   and ensuring `completeOperation()` is called.  public class AsynchronousOperation : NSOperation {      override public var asynchronous: Bool { return true }      private let stateLock = NSLock()      private var _executing: Bool = false     override private(set) public var executing: Bool {         get {             return stateLock.withCriticalScope { _executing }         }         set {             willChangeValueForKey("isExecuting")             stateLock.withCriticalScope { _executing = newValue }             didChangeValueForKey("isExecuting")         }     }      private var _finished: Bool = false     override private(set) public var finished: Bool {         get {             return stateLock.withCriticalScope { _finished }         }         set {             willChangeValueForKey("isFinished")             stateLock.withCriticalScope { _finished = newValue }             didChangeValueForKey("isFinished")         }     }      /// Complete the operation     ///     /// This will result in the appropriate KVN of isFinished and isExecuting      public func completeOperation() {         if executing {             executing = false         }          if !finished {             finished = true         }     }      override public func start() {         if cancelled {             finished = true             return         }          executing = true          main()     }      override public func main() {         fatalError("subclasses must override `main`")     } }  /*     Copyright (C) 2015 Apple Inc. All Rights Reserved.     See LICENSE.txt for this sample’s licensing information      Abstract:     An extension to `NSLock` to simplify executing critical code.      From Advanced NSOperations sample code in WWDC 2015 https://developer.apple.com/videos/play/wwdc2015/226/     From https://developer.apple.com/sample-code/wwdc/2015/downloads/Advanced-NSOperations.zip */  import Foundation  extension NSLock {      /// Perform closure within lock.     ///     /// An extension to `NSLock` to simplify executing critical code.     ///     /// - parameter block: The closure to be performed.      func withCriticalScope(@noescape block: Void -> T) -> T {         lock()         let value = block()         unlock()         return value     } }

Then you can do:

for url in urls {     queue.addOperation(DownloadOperation(session: session, url: url)) }

So that's one very easy way to wrap asynchronous URLSession/NSURLSession requests in asynchronous Operation/NSOperation subclass. More generally, this is a useful pattern, using AsynchronousOperation to wrap up some asynchronous task in an Operation/NSOperation object.

Unfortunately, in your question, you wanted to use delegate-based URLSession/NSURLSession so you could monitor the progress of the downloads. This is more complicated.

This is because the "task complete" NSURLSession delegate methods are called at the session object's delegate. This is an infuriating design feature of NSURLSession (but Apple did it to simplify background sessions, which isn't relevant here, but we're stuck with that design limitation).

But we have to asynchronously complete the operations as the tasks finish. So we need some way for the session to figure out with operation to complete when didCompleteWithError is called. Now you could have each operation have its own NSURLSession object, but it turns out that this is pretty inefficient.

So, to handle that, I maintain a dictionary, keyed by the task's taskIdentifier, which identifies the appropriate operation. That way, when the download finishes, you can "complete" the correct asynchronous operation. So, in Swift 3:

/// Manager of asynchronous download `Operation` objects  class DownloadManager: NSObject {      /// Dictionary of operations, keyed by the `taskIdentifier` of the `URLSessionTask`      fileprivate var operations = [Int: DownloadOperation]()      /// Serial NSOperationQueue for downloads      private let queue: OperationQueue = {         let _queue = OperationQueue()         _queue.name = "download"         _queue.maxConcurrentOperationCount = 1    // I'd usually use values like 3 or 4 for performance reasons, but OP asked about downloading one at a time          return _queue     }()      /// Delegate-based NSURLSession for DownloadManager      lazy var session: URLSession = {         let configuration = URLSessionConfiguration.default         return URLSession(configuration: configuration, delegate: self, delegateQueue: nil)     }()      /// Add download     ///     /// - parameter URL:  The URL of the file to be downloaded     ///     /// - returns:        The DownloadOperation of the operation that was queued      @discardableResult     func addDownload(_ url: URL) -> DownloadOperation {         let operation = DownloadOperation(session: session, url: url)         operations[operation.task.taskIdentifier] = operation         queue.addOperation(operation)         return operation     }      /// Cancel all queued operations      func cancelAll() {         queue.cancelAllOperations()     }  }  // MARK: URLSessionDownloadDelegate methods  extension DownloadManager: URLSessionDownloadDelegate {      func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {         operations[downloadTask.taskIdentifier]?.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location)     }      func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {         operations[downloadTask.taskIdentifier]?.urlSession(session, downloadTask: downloadTask, didWriteData: bytesWritten, totalBytesWritten: totalBytesWritten, totalBytesExpectedToWrite: totalBytesExpectedToWrite)     } }  // MARK: URLSessionTaskDelegate methods  extension DownloadManager: URLSessionTaskDelegate {      func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)  {         let key = task.taskIdentifier         operations[key]?.urlSession(session, task: task, didCompleteWithError: error)         operations.removeValue(forKey: key)     }  }  /// Asynchronous Operation subclass for downloading  class DownloadOperation : AsynchronousOperation {     let task: URLSessionTask      init(session: URLSession, url: URL) {         task = session.downloadTask(with: url)         super.init()     }      override func cancel() {         task.cancel()         super.cancel()     }      override func main() {         task.resume()     } }  // MARK: NSURLSessionDownloadDelegate methods  extension DownloadOperation: URLSessionDownloadDelegate {      func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {         do {             let manager = FileManager.default             let destinationURL = try manager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)                 .appendingPathComponent(downloadTask.originalRequest!.url!.lastPathComponent)             if manager.fileExists(atPath: destinationURL.path) {                 try manager.removeItem(at: destinationURL)             }             try manager.moveItem(at: location, to: destinationURL)         } catch {             print("\(error)")         }     }      func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {         let progress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)         print("\(downloadTask.originalRequest!.url!.absoluteString) \(progress)")     } }  // MARK: NSURLSessionTaskDelegate methods  extension DownloadOperation: URLSessionTaskDelegate {      func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)  {         completeOperation()         if error != nil {             print("\(error)")         }     }  }

Or in Swift 2:

/// Manager of asynchronous NSOperation objects  class         
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!