我有一个必须下载多个大文件的应用程序。我希望它依次而不是同时下载每个文件。当它同时运行时,该应用程序将过载并崩溃。
所以。我试图将downloadTaskWithURL包装在NSBlockOperation内,然后在队列上设置maxConcurrentOperationCount = 1。我在下面编写了此代码,但由于两个文件同时下载而无法正常工作。
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) } }
如何正确编写此代码以实现我一次只下载一个文件的目标。
您的代码无法正常URLSessionDownloadTask运行,因为它异步运行。因此,BlockOperation下载操作在下载完成之前完成,因此在依次触发操作的同时,下载任务将异步且并行地继续。
URLSessionDownloadTask
BlockOperation
为了解决这个问题,您可以将请求包装在异步Operation子类中。有关更多信息,请参见《 并发编程指南》 中的“为并发执行配置操作 ” 。
Operation
但是,在我说明如何根据您的情况(基于委托URLSession)来执行此操作之前,让我首先向您展示使用完成处理程序表示法时更简单的解决方案。稍后,我们将在此基础上解决您更复杂的问题。因此,在Swift 3及更高版本中:
URLSession
class DownloadOperation : AsynchronousOperation { var task: URLSessionTask! init(session: URLSession, url: URL) { super.init() task = session.downloadTask(with: url) { temporaryURL, response, error in defer { self.finish() } guard let httpResponse = response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else { // handle invalid return codes however you'd like return } guard let temporaryURL = temporaryURL, error == nil else { print(error ?? "Unknown 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 emits all of the necessary KVO notifications 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 `finish()` is called; or /// override `cancel` method, calling `super.cancel()` and then cleaning-up /// and ensuring `finish()` is called. class AsynchronousOperation: Operation { /// State for this operation. @objc private enum OperationState: Int { case ready case executing case finished } /// Concurrent queue for synchronizing access to `state`. private let stateQueue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".rw.state", attributes: .concurrent) /// Private backing stored property for `state`. private var rawState: OperationState = .ready /// The state of the operation @objc private dynamic var state: OperationState { get { return stateQueue.sync { rawState } } set { stateQueue.sync(flags: .barrier) { rawState = newValue } } } // MARK: - Various `Operation` properties open override var isReady: Bool { return state == .ready && super.isReady } public final override var isExecuting: Bool { return state == .executing } public final override var isFinished: Bool { return state == .finished } // KVO for dependent properties open override class func keyPathsForValuesAffectingValue(forKey key: String) -> Set<String> { if ["isReady", "isFinished", "isExecuting"].contains(key) { return [#keyPath(state)] } return super.keyPathsForValuesAffectingValue(forKey: key) } // Start public final override func start() { if isCancelled { finish() return } state = .executing main() } /// Subclasses must implement this to perform their work and they must not call `super`. The default implementation of this function throws an exception. open override func main() { fatalError("Subclasses must implement `main`.") } /// Call this function to finish an operation that is currently executing public final func finish() { if !isFinished { state = .finished } } }
然后,您可以执行以下操作:
for url in urls { queue.addOperation(DownloadOperation(session: session, url: url)) }
因此,这是将异步URLSession/ NSURLSession请求包装在异步Operation/ NSOperation子类中的一种非常简单的方法。更一般而言,这是一种有用的模式,AsynchronousOperation用于将一些异步任务包装在Operation/ NSOperation对象中。
NSURLSession
NSOperation
AsynchronousOperation
不幸的是,在您的问题中,您想使用基于委托的URLSession/,NSURLSession以便可以监视下载进度。这更复杂。
这是因为NSURLSession在会话对象的委托处调用了“任务完成” 委托方法。这是的令人毛骨悚然的设计功能NSURLSession(但是Apple这样做是为了简化后台会议,在这里不相关,但是我们受制于该设计限制)。
但是,我们必须在任务完成时异步完成操作。因此,我们需要某种方式让会话确定何时didCompleteWithError调用哪个操作才能完成。现在您可以使每个操作都有其自己的NSURLSession对象,但是事实证明这效率很低。
didCompleteWithError
因此,为了解决这个问题,我维护了一个字典,该字典以任务的键为关键字taskIdentifier,用于标识适当的操作。这样,下载完成后,您可以“完成”正确的异步操作。从而:
taskIdentifier
/// 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 OperationQueue 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 `URLSession` 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 queueDownload(_ 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) { guard let httpResponse = downloadTask.response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else { // handle invalid return codes however you'd like return } do { let manager = FileManager.default let destinationURL = try manager .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true) .appendingPathComponent(downloadTask.originalRequest!.url!.lastPathComponent) 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: URLSessionTaskDelegate methods extension DownloadOperation: URLSessionTaskDelegate { func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { defer { finish() } if let error = error { print(error) return } // do whatever you want upon success } }
然后像这样使用它:
let downloadManager = DownloadManager() override func viewDidLoad() { super.viewDidLoad() let urlStrings = [ "http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/s72-55482.jpg", "http://spaceflight.nasa.gov/gallery/images/apollo/apollo10/hires/as10-34-5162.jpg", "http://spaceflight.nasa.gov/gallery/images/apollo-soyuz/apollo-soyuz/hires/s75-33375.jpg", "http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-134-20380.jpg", "http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-140-21497.jpg", "http://spaceflight.nasa.gov/gallery/images/apollo/apollo17/hires/as17-148-22727.jpg" ] let urls = urlStrings.compactMap { URL(string: $0) } let completion = BlockOperation { print("all done") } for url in urls { let operation = downloadManager.queueDownload(url) completion.addDependency(operation) } OperationQueue.main.addOperation(completion) }