我有一个必须下载多个大文件的应用程序。我希望它依次而不是同时下载一个文件。当它同时运行时,该应用程序将过载并崩溃。
所以。我试图将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
下载操作在下载完成之前完成,因此在顺序触发操作的同时,下载任务将异步且并行地继续。
尽管可以考虑一些变通办法(例如,递归模式在前一个请求完成后启动一个请求,后台线程上的非零信号量模式等),但优雅的解决方案是一种成熟的异步框架之一。从历史上看,如果您想控制一系列异步任务的并发程度,我们将寻求一个异步Operation
子类。如今,在iOS 13及更高版本中,我们可以考虑Combine。(还有其他第三方异步编程框架,但我将仅限于Apple提供的方法。)
运作方式
为了解决这个问题,您可以将请求包装在异步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 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)
}
查看Swift 2实施的修订历史记录。
结合
对于合并的想法是创建一个Publisher
为URLSessionDownloadTask
。然后,您可以执行以下操作:
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
(将全部资产加载到内存中,这对于大型资产来说是不可接受的解决方案),但是可以参考其源代码并对其进行修改以创建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
失败,而是replaceError
使用nil
:
/// 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()
}
本文收集自互联网,转载请注明来源。
如有侵权,请联系 [email protected] 删除。
我来说两句