【问题标题】:How To Download Multiple Files Sequentially using NSURLSession downloadTask in Swift如何在 Swift 中使用 NSURLSession downloadTask 按顺序下载多个文件
【发布时间】:2015-11-26 03:29:36
【问题描述】:

我有一个必须下载多个大文件的应用。我希望它按顺序而不是同时下载每个文件。当它同时运行时,应用程序会过载并崩溃。

所以。我试图将 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)
    }

}

如何正确编写此代码以实现我一次只下载一个文件的目标。

【问题讨论】:

    标签: ios swift multithreading nsurlsession nsoperation


    【解决方案1】:

    您的代码将无法运行,因为URLSessionDownloadTask 异步运行。因此,BlockOperation 在下载完成之前完成,因此当操作按顺序触发时,下载任务将异步并行继续。

    虽然可以考虑一些变通方法(例如,递归模式在前一个请求完成后启动一个请求,后台线程上的非零信号量模式等),但优雅的解决方案是经过验证的异步框架之一。

    在 iOS 15 及更高版本中,我们将使用async-await 方法download(from:delegate:),例如

    func downloadFiles() async throws {
        let folder = try! FileManager.default
            .url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
    
        for url in urls {
            let (source, _) = try await URLSession.shared.download(from: url)
            let destination = folder.appendingPathComponent(url.lastPathComponent)
            try FileManager.default.moveItem(at: source, to: destination)
        }
    }
    

    在哪里

    override func viewDidLoad() {
        super.viewDidLoad()
    
        Task {
            do {
                try await downloadFiles()
            } catch {
                print(error)
            }
        }
    }
    

    这仅适用于 iOS 15 及更高版本。但 Xcode 13.2 及更高版本实际上允许您在 iOS 13 中使用 async-await,但您只需要编写自己的 asyncdownload 的再现:

    extension URLSession {
        @available(iOS, deprecated: 15, message: "Use `download(from:delegate:)` instead")
        func download(with url: URL) async throws -> (URL, URLResponse) {
            try await download(with: URLRequest(url: url))
        }
    
        @available(iOS, deprecated: 15, message: "Use `download(for:delegate:)` instead")
        func download(with request: URLRequest) async throws -> (URL, URLResponse) {
            let sessionTask = URLSessionTaskActor()
    
            return try await withTaskCancellationHandler {
                Task { await sessionTask.cancel() }
            } operation: {
                try await withCheckedThrowingContinuation { continuation in
                    Task {
                        await sessionTask.start(downloadTask(with: request) { location, response, error in
                            guard let location = location, let response = response else {
                                continuation.resume(throwing: error ?? URLError(.badServerResponse))
                                return
                            }
    
                            // since continuation can happen later, let's figure out where to store it ...
    
                            let tempURL = URL(fileURLWithPath: NSTemporaryDirectory())
                                .appendingPathComponent(UUID().uuidString)
                                .appendingPathExtension(request.url!.pathExtension)
    
                            // ... and move it to there
    
                            do {
                                try FileManager.default.moveItem(at: location, to: tempURL)
                            } catch {
                                continuation.resume(throwing: error)
                                return
                            }
    
                            continuation.resume(returning: (tempURL, response))
                        })
                    }
                }
            }
        }
    }
    
    private extension URLSession {
        actor URLSessionTaskActor {
            weak var task: URLSessionTask?
    
            func start(_ task: URLSessionTask) {
                self.task = task
                task.resume()
            }
    
            func cancel() {
                task?.cancel()
            }
        }
    }
    

    然后,您可以为 iOS 13 及更高版本调用此版本:

    func downloadFiles() async throws {
        let folder = try! FileManager.default
            .url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
    
        for url in urls {
            let (source, _) = try await URLSession.shared.download(with: url)
            let destination = folder.appendingPathComponent(url.lastPathComponent)
            try FileManager.default.moveItem(at: source, to: destination)
        }
    }
    

    在 13 之前的 iOS 版本中,如果你想控制一系列异步任务的并发程度,我们会使用异步 Operation 子类。

    或者,在 iOS 13 及更高版本中,您也可以考虑使用Combine。 (还有其他第三方异步编程框架,但我会限制自己使用 Apple 提供的方法。)

    这两个都在我的原始答案中描述如下。


    操作

    为了解决这个问题,您可以将请求包装在异步 Operation 子类中。更多信息请参阅并发编程指南中的Configuring Operations for Concurrent Execution

    但在我说明如何在您的情况下执行此操作(基于委托的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 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 对象中。

    不幸的是,在您的问题中,您想使用基于委托的URLSession/NSURLSession,以便您可以监控下载进度。这个比较复杂。

    这是因为“任务完成”NSURLSession 委托方法在会话对象的委托处被调用。这是NSURLSession 的一个令人恼火的设计功能(但 Apple 这样做是为了简化后台会话,这与这里无关,但我们被这个设计限制所困扰)。

    但是我们必须在任务完成时异步完成操作。所以我们需要一些方法让会话确定在调用didCompleteWithError 时要完成哪个操作。现在您可以让每个操作都有自己的NSURLSession 对象,但事实证明这非常低效。

    因此,为了处理这个问题,我维护了一个字典,由任务的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)
    }
    

    请参阅revision history 了解 Swift 2 的实现。


    合并

    对于Combine,我们的想法是为URLSessionDownloadTask 创建一个Publisher。然后您可以执行以下操作:

    var downloadRequests: AnyCancellable?
    
    /// Download a series of assets
    
    func downloadAssets() {
        downloadRequests = downloadsPublisher(for: urls, maxConcurrent: 1).sink { completion in
            switch completion {
            case .finished:
                print("done")
    
            case .failure(let error):
                print("failed", error)
            }
        } receiveValue: { destinationUrl in
            print(destinationUrl)
        }
    }
    
    /// Publisher for single download
    ///
    /// Copy downloaded resource to caches folder.
    ///
    /// - Parameter url: `URL` being downloaded.
    /// - Returns: Publisher for the URL with final destination of the downloaded asset.
    
    func downloadPublisher(for url: URL) -> AnyPublisher<URL, Error> {
        URLSession.shared.downloadTaskPublisher(for: url)
            .tryCompactMap {
                let destination = try FileManager.default
                    .url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
                    .appendingPathComponent(url.lastPathComponent)
                try FileManager.default.moveItem(at: $0.location, to: destination)
                return destination
            }
            .receive(on: RunLoop.main)
            .eraseToAnyPublisher()
    }
    
    /// Publisher for a series of downloads
    ///
    /// This downloads not more than `maxConcurrent` assets at a given time.
    ///
    /// - Parameters:
    ///   - urls: Array of `URL`s of assets to be downloaded.
    ///   - maxConcurrent: The maximum number of downloads to run at any given time (default 4).
    /// - Returns: Publisher for the URLs with final destination of the downloaded assets.
    
    func downloadsPublisher(for urls: [URL], maxConcurrent: Int = 4) -> AnyPublisher<URL, Error> {
        Publishers.Sequence(sequence: urls.map { downloadPublisher(for: $0) })
            .flatMap(maxPublishers: .max(maxConcurrent)) { $0 }
            .eraseToAnyPublisher()
    }
    

    现在,不幸的是,Apple 提供了一个 DataTaskPublisher(它将整个资产加载到内存中,这对于大型资产来说是不可接受的解决方案),但可以参考 their source code 并对其进行调整以创建一个 DownloadTaskPublisher

    //  DownloadTaskPublisher.swift
    //
    //  Created by Robert Ryan on 9/28/20.
    //
    //  Adapted from Apple's `DataTaskPublisher` at:
    //  https://github.com/apple/swift/blob/88b093e9d77d6201935a2c2fb13f27d961836777/stdlib/public/Darwin/Foundation/Publishers%2BURLSession.swift
    
    import Foundation
    import Combine
    
    // MARK: Download Tasks
    
    @available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
    extension URLSession {
        /// Returns a publisher that wraps a URL session download task for a given URL.
        ///
        /// The publisher publishes temporary when the task completes, or terminates if the task fails with an error.
        ///
        /// - Parameter url: The URL for which to create a download task.
        /// - Returns: A publisher that wraps a download task for the URL.
    
        public func downloadTaskPublisher(for url: URL) -> DownloadTaskPublisher {
            let request = URLRequest(url: url)
            return DownloadTaskPublisher(request: request, session: self)
        }
    
        /// Returns a publisher that wraps a URL session download task for a given URL request.
        ///
        /// The publisher publishes download when the task completes, or terminates if the task fails with an error.
        ///
        /// - Parameter request: The URL request for which to create a download task.
        /// - Returns: A publisher that wraps a download task for the URL request.
    
        public func downloadTaskPublisher(for request: URLRequest) -> DownloadTaskPublisher {
            return DownloadTaskPublisher(request: request, session: self)
        }
    
        public struct DownloadTaskPublisher: Publisher {
            public typealias Output = (location: URL, response: URLResponse)
            public typealias Failure = URLError
    
            public let request: URLRequest
            public let session: URLSession
    
            public init(request: URLRequest, session: URLSession) {
                self.request = request
                self.session = session
            }
    
            public func receive<S: Subscriber>(subscriber: S) where Failure == S.Failure, Output == S.Input {
                subscriber.receive(subscription: Inner(self, subscriber))
            }
    
            private typealias Parent = DownloadTaskPublisher
            private final class Inner<Downstream: Subscriber>: Subscription, CustomStringConvertible, CustomReflectable, CustomPlaygroundDisplayConvertible
            where
                Downstream.Input == Parent.Output,
                Downstream.Failure == Parent.Failure
            {
                typealias Input = Downstream.Input
                typealias Failure = Downstream.Failure
    
                private let lock: NSLocking
                private var parent: Parent?               // GuardedBy(lock)
                private var downstream: Downstream?       // GuardedBy(lock)
                private var demand: Subscribers.Demand    // GuardedBy(lock)
                private var task: URLSessionDownloadTask! // GuardedBy(lock)
                var description: String { return "DownloadTaskPublisher" }
                var customMirror: Mirror {
                    lock.lock()
                    defer { lock.unlock() }
                    return Mirror(self, children: [
                        "task": task as Any,
                        "downstream": downstream as Any,
                        "parent": parent as Any,
                        "demand": demand,
                    ])
                }
                var playgroundDescription: Any { return description }
    
                init(_ parent: Parent, _ downstream: Downstream) {
                    self.lock = NSLock()
                    self.parent = parent
                    self.downstream = downstream
                    self.demand = .max(0)
                }
    
                // MARK: - Upward Signals
                func request(_ d: Subscribers.Demand) {
                    precondition(d > 0, "Invalid request of zero demand")
    
                    lock.lock()
                    guard let p = parent else {
                        // We've already been cancelled so bail
                        lock.unlock()
                        return
                    }
    
                    // Avoid issues around `self` before init by setting up only once here
                    if self.task == nil {
                        let task = p.session.downloadTask(
                            with: p.request,
                            completionHandler: handleResponse(location:response:error:)
                        )
                        self.task = task
                    }
    
                    self.demand += d
                    let task = self.task!
                    lock.unlock()
    
                    task.resume()
                }
    
                private func handleResponse(location: URL?, response: URLResponse?, error: Error?) {
                    lock.lock()
                    guard demand > 0,
                          parent != nil,
                          let ds = downstream
                    else {
                        lock.unlock()
                        return
                    }
    
                    parent = nil
                    downstream = nil
    
                    // We clear demand since this is a single shot shape
                    demand = .max(0)
                    task = nil
                    lock.unlock()
    
                    if let location = location, let response = response, error == nil {
                        _ = ds.receive((location, response))
                        ds.receive(completion: .finished)
                    } else {
                        let urlError = error as? URLError ?? URLError(.unknown)
                        ds.receive(completion: .failure(urlError))
                    }
                }
    
                func cancel() {
                    lock.lock()
                    guard parent != nil else {
                        lock.unlock()
                        return
                    }
                    parent = nil
                    downstream = nil
                    demand = .max(0)
                    let task = self.task
                    self.task = nil
                    lock.unlock()
                    task?.cancel()
                }
            }
        }
    }
    

    现在,不幸的是,这不是使用 URLSession 委托模式,而是使用完成处理程序再现。但可以想象,它可以适应委托模式。

    此外,当下载失败时,这将停止下载。如果你不希望它仅仅因为一个失败而停止,你可以想象将它定义为Never失败,而不是replaceErrornil

    /// Publisher for single download
    ///
    /// Copy downloaded resource to caches folder.
    ///
    /// - Parameter url: `URL` being downloaded.
    /// - Returns: Publisher for the URL with final destination of the downloaded asset. Returns `nil` if request failed.
    
    func downloadPublisher(for url: URL) -> AnyPublisher<URL?, Never> {
        URLSession.shared.downloadTaskPublisher(for: url)
            .tryCompactMap {
                let destination = try FileManager.default
                    .url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
                    .appendingPathComponent(url.lastPathComponent)
                try FileManager.default.moveItem(at: $0.location, to: destination)
                return destination
            }
            .replaceError(with: nil)
            .receive(on: RunLoop.main)
            .eraseToAnyPublisher()
    }
    
    /// Publisher for a series of downloads
    ///
    /// This downloads not more than `maxConcurrent` assets at a given time.
    ///
    /// - Parameters:
    ///   - urls: Array of `URL`s of assets to be downloaded.
    ///   - maxConcurrent: The maximum number of downloads to run at any given time (default 4).
    /// - Returns: Publisher for the URLs with final destination of the downloaded assets.
    
    func downloadsPublisher(for urls: [URL], maxConcurrent: Int = 4) -> AnyPublisher<URL?, Never> {
        Publishers.Sequence(sequence: urls.map { downloadPublisher(for: $0) })
            .flatMap(maxPublishers: .max(maxConcurrent)) { $0 }
            .eraseToAnyPublisher()
    }
    

    也许不用说,我通常不鼓励按顺序下载资产/文件。您应该允许它们同时运行,但要控制并发程度,以免您的应用程序过载。上面概述的所有模式都将并发程度限制在合理的范围内。

    【讨论】:

    • 顺便说一句,this answer 展示了如何在异步操作中包装 Alamofire 请求。包装 NSURLSessionDownloadTask 请求时的模式也类似。另外,仅供参考,这种NSOperation 技术在处理后台会话时不起作用。在这些情况下,您必须走老路,在另一个请求的完成处理程序中启动下一个请求。
    • 我从来没有得到这个工作,但我不接受这是正确的答案。
    • @CraigH - 我已经扩展了我的答案,向您展示了您将如何做到这一点。我犹豫是否将所有这些都包含在我的原始答案中,因为它有点多,但考虑到你在实现这个时遇到了问题(我完全同情......这很复杂),我已经添加了相关的代码示例。
    • 哇!多么令人振奋的答案。它为您提供了如何同时下载多个文件的全部逻辑。这并不意味着您不能以其他方式执行此操作,但逻辑将是相同的。
    • @VarunNaharia - BlockOperation 是一个操作对象,它包装了一个闭包(我们现在在 Swift 中称为“闭包”,在 Objective-C 中过去称为“块”,因此 @ 987654378@ 姓名)。所以BlockOperation 只是一种表达方式:“这里有一些代码,我稍后会添加到一些操作队列中。”如果您熟悉 GCD 中的 DispatchWorkItem,则 BlockOperation 是等效的操作队列。
    【解决方案2】:

    这是相当简约和纯粹的快速方法。没有 NSOperationQueue(),只有 didSet-observer

        import Foundation
    
    
        class DownloadManager {
    
            var delegate: HavingWebView?
            var gotFirstAndEnough = true
            var finalURL: NSURL?{
                didSet{
                    if finalURL != nil {
                        if let s = self.contentOfURL{
                            self.delegate?.webView.loadHTMLString(s, baseURL: nil)
                        }
                    }
                }
            }
            var lastRequestBeginning: NSDate?
    
            var myLinks = [String](){
                didSet{
                    self.handledLink = self.myLinks.count
                }
            }
    
            var contentOfURL: String?
    
            var handledLink = 0 {
                didSet{
                    if handledLink == 0 {
                        self.finalURL = nil
                        print("????????????")
                    } else {
                        if self.finalURL == nil {
                            if let nextURL = NSURL(string: self.myLinks[self.handledLink-1]) {
                                self.loadAsync(nextURL)
                            }
                        }
                    }
                }
            }
    
            func loadAsync(url: NSURL) {
                let sessionConfig = NSURLSessionConfiguration.ephemeralSessionConfiguration()
                let session = NSURLSession(configuration: sessionConfig, delegate: nil, delegateQueue: NSOperationQueue.mainQueue())
                let request = NSMutableURLRequest(URL: url, cachePolicy: NSURLRequestCachePolicy.ReloadIgnoringCacheData, timeoutInterval: 15.0)
                request.HTTPMethod = "GET"
                print("?")
                self.lastRequestBeginning = NSDate()
                print("Requet began:    \(self.lastRequestBeginning )")
                let task = session.dataTaskWithRequest(request, completionHandler: { (data: NSData?, response: NSURLResponse?, error: NSError?) -> Void in
                    if (error == nil) {
                        if let response = response as? NSHTTPURLResponse {
                            print("\(response)")
                            if response.statusCode == 200 {
                                if let content = String(data: data!, encoding: NSUTF8StringEncoding) {
                                    self.contentOfURL = content
                                }
                                self.finalURL =  url
                            }
                        }
                    }
                    else {
                        print("Failure: \(error!.localizedDescription)");
                    }
    
                    let elapsed = NSDate().timeIntervalSinceDate(self.lastRequestBeginning!)
                    print("trying \(url) takes \(elapsed)")
                    print("?   Request finished")
                    print("____________________________________________")
                    self.handledLink -= 1
                })
                task.resume()
            }
        }
    

    在 ViewController 中:

    protocol HavingWebView {
        var webView: UIWebView! {get set}
    }
    
    
    class ViewController: UIViewController, HavingWebView {
    
        @IBOutlet weak var webView: UIWebView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            // Do any additional setup after loading the view, typically from a nib.
            let dm = DownloadManager()
            dm.delegate = self
            dm.myLinks =  ["https://medium.com/the-mission/consider-the-present-and-future-value-of-your-decisions-b20fb72f5e#.a12uiiz11",
                           "https://medium.com/@prianka.kariat/ios-10-notifications-with-attachments-and-much-more-169a7405ddaf#.svymi6230",
                           "https://blog.medium.com/39-reasons-we-wont-soon-forget-2016-154ac95683af#.cmb37i58b",
                           "https://backchannel.com/in-2017-your-coworkers-will-live-everywhere-ae14979b5255#.wmi6hxk9p"]
        }
    
    
    
    }
    

    【讨论】:

      【解决方案3】:

      Rob 的回答显示了正确的方法。 我通过基于委托的方式实现了它,通过进度视图跟踪下载。

      您可以在此处查看源代码。 Multiple download with progress bar (Github)

      【讨论】:

        【解决方案4】:

        后台情况不止一个代码。我可以通过使用的全局变量和 NSTimer 来学习。你也可以试试。

        定义“indexDownloaded”全局变量。

        import UIKit
        import Foundation
        
        private let _sharedUpdateStatus = UpdateStatus()
        class UpdateStatus : NSObject  {
        
        // MARK: - SHARED INSTANCE
        class var shared : UpdateStatus {
            return _sharedUpdateStatus
        }
          var indexDownloaded = 0
        }
        

        此代码添加到 DownloadOperation 类中。

        print("⬇️" + URL.lastPathComponent! + " downloaded")
                UpdateStatus.shared.indexDownloaded += 1
                print(String(UpdateStatus.shared.indexDownloaded) + "\\" + String(UpdateStatus.shared.count))
        

        这个函数在你的 viewController 中。

        func startTimeAction () {
            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.flatMap { URL(string: $0) }
        
            for url in urls {
               queue.addOperation(DownloadOperation(session: session, url: url))
            }
        
            UpdateStatus.shared.count = urls.count
             progressView.setProgress(0.0, animated: false)
            timer.invalidate()
            timer = NSTimer.scheduledTimerWithTimeInterval(0.2, target: self, selector: #selector(timeAction), userInfo: nil, repeats: true)
        }
        
        func timeAction() {
            if UpdateStatus.shared.count != 0 {
                let set: Float = Float(UpdateStatus.shared.indexDownloaded) / Float(UpdateStatus.shared.count)
        
                progressView.setProgress(set, animated: true)
            }
        

        这样,通过更新progressview会查看每次定时器运行的下载次数。

        【讨论】:

          【解决方案5】:

          Objective-C 版本是:

          [operation2 addDependency:operation1]
          

          【讨论】:

          • 更重要的是,在处理异步任务时,使用依赖并不比在队列上使用maxConcurrentOperationCount1 好。
          猜你喜欢
          • 2019-02-14
          • 1970-01-01
          • 1970-01-01
          • 2020-10-12
          • 2017-10-14
          • 2019-12-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多