J'ai une application qui doit télécharger plusieurs fichiers volumineux. Je veux qu'il télécharge chaque fichier un par un séquentiellement au lieu de simultanément. Lorsqu'il s'exécute simultanément, l'application est surchargée et se bloque.
Donc. Im essayant d'encapsuler un downloadTaskWithURL dans un NSBlockOperation puis de définir le maxConcurrentOperationCount = 1 dans la file d'attente. J'ai écrit ce code ci-dessous mais cela n'a pas fonctionné car les deux fichiers sont téléchargés simultanément.
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)
}
}
Comment peut-il écrire correctement pour atteindre mon objectif de ne télécharger qu'un seul fichier à la fois.
Votre code ne fonctionnera pas car URLSessionDownloadTask
s'exécute de manière asynchrone. Ainsi, BlockOperation
se termine avant que le téléchargement ne soit terminé et donc pendant que les opérations se déclenchent séquentiellement, les tâches de téléchargement continueront de manière asynchrone et en parallèle.
Pour résoudre ce problème, vous pouvez encapsuler les demandes dans la sous-classe asynchrone Operation
. Voir Configuration des opérations pour l'exécution simultanée dans le Guide de programmation des accès concurrents pour plus d'informations.
Mais avant d'illustrer comment procéder dans votre situation (le délégué URLSession
), permettez-moi d'abord de vous montrer la solution la plus simple lors de l'utilisation du rendu du gestionnaire d'achèvement. Nous développerons plus tard cela pour votre question plus compliquée. Donc, dans Swift 3 et versions ultérieures:
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 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()
}
}
Où
/// Asynchronous operation base class
///
/// This is abstract to class performs all of the necessary KVN 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 }
// KVN 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 }
}
}
Ensuite, vous pouvez faire:
for url in urls {
queue.addOperation(DownloadOperation(session: session, url: url))
}
C'est donc un moyen très simple d'encapsuler les demandes asynchrones URLSession
/NSURLSession
dans la sous-classe asynchrone Operation
/NSOperation
. Plus généralement, il s'agit d'un modèle utile, utilisant AsynchronousOperation
pour conclure une tâche asynchrone dans un objet Operation
/NSOperation
.
Malheureusement, dans votre question, vous vouliez utiliser les URLSession
/NSURLSession
basés sur les délégués pour pouvoir suivre la progression des téléchargements. C'est plus compliqué.
Cela est dû au fait que les méthodes de délégué "tâche terminée" NSURLSession
sont appelées au niveau du délégué de l'objet de session. Il s'agit d'une caractéristique de conception exaspérante de NSURLSession
(mais Apple l'a fait pour simplifier les sessions d'arrière-plan, ce qui n'est pas pertinent ici, mais nous sommes coincés avec cette limitation de conception).
Mais nous devons terminer les opérations de manière asynchrone à la fin des tâches. Nous avons donc besoin d'un moyen pour que la session comprenne que l'opération se termine lorsque didCompleteWithError
est appelé. Maintenant, chaque opération pourrait avoir son propre objet NSURLSession
, mais il s'avère que c'est assez inefficace.
Donc, pour gérer cela, je maintiens un dictionnaire, saisi par taskIdentifier
de la tâche, qui identifie l'opération appropriée. De cette façon, une fois le téléchargement terminé, vous pouvez "terminer" l'opération asynchrone correcte. Donc:
/// 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) {
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
}
}
Et puis utilisez-le comme ceci:
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)
}
Voir historique des révisions pour Swift 2 implémentation.
Voici une approche plutôt minimaliste et purement Swift. Sans NSOperationQueue (), juste 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()
}
}
Dans 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://myerotica.com/jingle-bell-fuck-the-twins-5a48782bf5f1#.mjqz821yo",
"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"]
}
}
Plus d'un code dans la situation d'arrière-plan. Je peux apprendre par la variable globale utilisée et NSTimer. Vous pouvez aussi essayer.
Définissez la variable globale "indexDownloaded".
import UIKit
import Foundation
private let _sharedUpdateStatus = UpdateStatus()
class UpdateStatus : NSObject {
// MARK: - SHARED INSTANCE
class var shared : UpdateStatus {
return _sharedUpdateStatus
}
var indexDownloaded = 0
}
Ce code s'ajoute à la classe DownloadOperation.
print("⬇️" + URL.lastPathComponent! + " downloaded")
UpdateStatus.shared.indexDownloaded += 1
print(String(UpdateStatus.shared.indexDownloaded) + "\\" + String(UpdateStatus.shared.count))
Cette fonction dans votre 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)
}
de cette manière, en mettant à jour la progressview, vous regarderez le nombre de téléchargements à chaque exécution du timer.