gpt4 book ai didi

ios - 如何在 Swift 中使用 NSURLSession downloadTask 顺序下载多个文件

转载 作者:IT王子 更新时间:2023-10-29 05:08:12 27 4
gpt4 key购买 nike

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

所以。我试图将 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 中。子类。见 Configuring Operations for Concurrent Execution在并发编程指南中了解更多信息。
但在我说明如何在您的情况下执行此操作之前(基于委托(delegate)的 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子类。更一般地说,这是一个有用的模式,使用 AsynchronousOperationOperation 中结束一些异步任务/ NSOperation目的。
不幸的是,在您的问题中,您想使用基于委托(delegate)的 URLSession/ NSURLSession这样您就可以监控下载进度。这个比较复杂。
这是因为“任务完成” NSURLSession委托(delegate)方法在 session 对象的委托(delegate)中被调用。这是 NSURLSession的一个令人发指的设计特征(但 Apple 这样做是为了简化后台 session ,这在此处无关紧要,但我们坚持该设计限制)。
但是我们必须在任务完成时异步完成操作。因此,我们需要某种方式让 session 在 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 ,想法是创建一个 PublisherURLSessionDownloadTask .然后你可以做这样的事情:
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 (将完整 Assets 加载到内存中,这对于大 Assets 来说是 Not Acceptable 解决方案),但可以引用 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委托(delegate)模式,而是完成处理程序的再现。但是可以想象,它可以适应委托(delegate)模式。
此外,这将在失败时停止下载。如果你不希望它仅仅因为一个失败而停止,你可以想象将它定义为 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()
}

关于ios - 如何在 Swift 中使用 NSURLSession downloadTask 顺序下载多个文件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/32322386/

27 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com