您的代码无法正常
URLSessionDownloadTask运行,因为它异步运行。因此,
BlockOperation下载操作在下载完成之前完成,因此在依次触发操作的同时,下载任务将异步且并行地继续。
为了解决这个问题,您可以将请求包装在异步
Operation子类中。有关更多信息,请参见《 并发编程指南》
中的“为并发执行配置操作
” 。
但是,在我说明如何根据您的情况(基于委托
URLSession)来执行此操作之前,让我首先向您展示使用完成处理程序表示法时更简单的解决方案。稍后,我们将在此基础上解决您更复杂的问题。因此,在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.finish() } guard let httpResponse = response as? HTTPURLResponse, 200..<300 ~= httpResponse.statusCode else { // handle invalid return pres 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对象中。
不幸的是,在您的问题中,您想使用基于委托的
URLSession/,
NSURLSession以便可以监视下载进度。这更复杂。
这是因为
NSURLSession在会话对象的委托处调用了“任务完成”
委托方法。这是的令人毛骨悚然的设计功能
NSURLSession(但是Apple这样做是为了简化后台会议,在这里不相关,但是我们受制于该设计限制)。
但是,我们必须在任务完成时异步完成操作。因此,我们需要某种方式让会话确定何时
didCompleteWithError调用哪个操作才能完成。现在您可以使每个操作都有其自己的
NSURLSession对象,但是事实证明这效率很低。
因此,为了解决这个问题,我维护了一个字典,该字典以任务的键为关键字
taskIdentifier,用于标识适当的操作。这样,下载完成后,您可以“完成”正确的异步操作。从而:
/// Manager of asynchronous download `Operation` objectsclass 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 methodsextension 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 methodsextension 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 downloadingclass 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 methodsextension 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 pres 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 methodsextension 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)}


