Ich habe ein seltsames Verhalten mit meiner Test-App. Ich habe ungefähr 50 gleichzeitige GET-Anforderungen, die ich an denselben Server sende. Der Server ist ein eingebetteter Server auf einer kleinen Hardware mit sehr begrenzten Ressourcen. Um die Leistung für jede einzelne Anforderung zu optimieren, konfiguriere ich eine Instanz Alamofire.Manager
wie folgt:
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
configuration.HTTPMaximumConnectionsPerHost = 2
configuration.timeoutIntervalForRequest = 30
let manager = Alamofire.Manager(configuration: configuration)
Wenn ich die Anfragen mit sende, werden manager.request(...)
sie in Zweierpaaren versandt (wie erwartet mit Charles HTTP Proxy überprüft). Das Seltsame ist jedoch, dass alle Anfragen, die nicht innerhalb von 30 Sekunden nach der ersten Anfrage abgeschlossen wurden, aufgrund des Zeitlimits gleichzeitig storniert werden (auch wenn sie noch nicht gesendet wurden). Hier ist eine Illustration, die das Verhalten zeigt:
Ist dies ein erwartetes Verhalten und wie kann ich sicherstellen, dass die Anforderungen nicht das Zeitlimit erhalten, bevor sie überhaupt gesendet werden?
Vielen Dank!
, not
timeoutIntervalForRequest " festlegen?Antworten:
Ja, dies ist das erwartete Verhalten. Eine Lösung besteht darin, Ihre Anforderungen in eine benutzerdefinierte, asynchrone
NSOperation
Unterklasse zu packen und dann mithilfemaxConcurrentOperationCount
der Operationswarteschlange die Anzahl der gleichzeitigen Anforderungen anstelle der zu steuernHTTPMaximumConnectionsPerHost
Parameters .Das ursprüngliche AFNetworking hat die Anforderungen in Betrieb genommen, was dies trivial machte. Die
NSURLSession
Implementierung von AFNetworking hat dies jedoch nie getan, ebenso wenig wie Alamofire.Sie können das einfach
Request
in eineNSOperation
Unterklasse einschließen. Zum Beispiel:class NetworkOperation: AsynchronousOperation { // define properties to hold everything that you'll supply when you instantiate // this object and will be used when the request finally starts // // in this example, I'll keep track of (a) URL; and (b) closure to call when request is done private let urlString: String private var networkOperationCompletionHandler: ((_ responseObject: Any?, _ error: Error?) -> Void)? // we'll also keep track of the resulting request operation in case we need to cancel it later weak var request: Alamofire.Request? // define init method that captures all of the properties to be used when issuing the request init(urlString: String, networkOperationCompletionHandler: ((_ responseObject: Any?, _ error: Error?) -> Void)? = nil) { self.urlString = urlString self.networkOperationCompletionHandler = networkOperationCompletionHandler super.init() } // when the operation actually starts, this is the method that will be called override func main() { request = Alamofire.request(urlString, method: .get, parameters: ["foo" : "bar"]) .responseJSON { response in // do whatever you want here; personally, I'll just all the completion handler that was passed to me in `init` self.networkOperationCompletionHandler?(response.result.value, response.result.error) self.networkOperationCompletionHandler = nil // now that I'm done, complete this operation self.completeOperation() } } // we'll also support canceling the request, in case we need it override func cancel() { request?.cancel() super.cancel() } }
Wenn ich dann meine 50 Anfragen initiieren möchte, würde ich so etwas tun:
let queue = OperationQueue() queue.maxConcurrentOperationCount = 2 for i in 0 ..< 50 { let operation = NetworkOperation(urlString: "http://example.com/request.php?value=\(i)") { responseObject, error in guard let responseObject = responseObject else { // handle error here print("failed: \(error?.localizedDescription ?? "Unknown error")") return } // update UI to reflect the `responseObject` finished successfully print("responseObject=\(responseObject)") } queue.addOperation(operation) }
Auf diese Weise werden diese Anforderungen durch die eingeschränkt
maxConcurrentOperationCount
, und wir müssen uns keine Sorgen machen, dass die Anforderungen abgelaufen sind.Dies ist eine beispielhafte
AsynchronousOperation
Basisklasse, die sich um die KVN kümmert, die der asynchronen / gleichzeitigenNSOperation
Unterklasse zugeordnet ist:// // AsynchronousOperation.swift // // Created by Robert Ryan on 9/20/14. // Copyright (c) 2014 Robert Ryan. All rights reserved. // import Foundation /// Asynchronous Operation base class /// /// This class performs all of the necessary KVN of `isFinished` and /// `isExecuting` for a concurrent `NSOperation` subclass. So, to developer /// a concurrent NSOperation subclass, you instead subclass this class which: /// /// - must override `main()` with the tasks that initiate the asynchronous task; /// /// - must call `completeOperation()` function when the asynchronous task is done; /// /// - optionally, periodically check `self.cancelled` status, performing any clean-up /// necessary and then ensuring that `completeOperation()` is called; or /// override `cancel` method, calling `super.cancel()` and then cleaning-up /// and ensuring `completeOperation()` is called. public class AsynchronousOperation : Operation { private let stateLock = NSLock() private var _executing: Bool = false override private(set) public var isExecuting: Bool { get { return stateLock.withCriticalScope { _executing } } set { willChangeValue(forKey: "isExecuting") stateLock.withCriticalScope { _executing = newValue } didChangeValue(forKey: "isExecuting") } } private var _finished: Bool = false override private(set) public var isFinished: Bool { get { return stateLock.withCriticalScope { _finished } } set { willChangeValue(forKey: "isFinished") stateLock.withCriticalScope { _finished = newValue } didChangeValue(forKey: "isFinished") } } /// Complete the operation /// /// This will result in the appropriate KVN of isFinished and isExecuting public func completeOperation() { if isExecuting { isExecuting = false } if !isFinished { isFinished = true } } override public func start() { if isCancelled { isFinished = true return } isExecuting = true main() } override public func main() { fatalError("subclasses must override `main`") } } /* Abstract: An extension to `NSLocking` to simplify executing critical code. Adapted from Advanced NSOperations sample code in WWDC 2015 https://developer.apple.com/videos/play/wwdc2015/226/ Adapted from https://developer.apple.com/sample-code/wwdc/2015/downloads/Advanced-NSOperations.zip */ import Foundation extension NSLocking { /// Perform closure within lock. /// /// An extension to `NSLocking` to simplify executing critical code. /// /// - parameter block: The closure to be performed. func withCriticalScope<T>(block: () throws -> T) rethrows -> T { lock() defer { unlock() } return try block() } }
Es gibt andere mögliche Variationen dieses Musters, aber stellen Sie einfach sicher, dass Sie (a) zurückkehren
true
fürasynchronous
; und (b) Sie veröffentlichen die erforderlicheisFinished
und dieisExecuting
KVN wie im Abschnitt Konfigurieren von Vorgängen für die gleichzeitige Ausführung des Concurrency Programming Guide: Operation Queues beschrieben .quelle
AsynchronousOperation
den Abschlussnil
nach dem Ausführen auf setzt und dadurch starke Referenzzyklen auflöst.maxConcurrentOperationCount = 2
hast, aber du hast 50 Mal angerufen ..." Das ist der springende Punkt: Das OP wollte 50 Anfragen in die Warteschlange stellen, aber nie mehr als zwei gleichzeitig laufen lassen. Das bestimmtmaxConcurrentOperationCount
einfach, wie viele zu einem bestimmten Zeitpunkt ausgeführt werden können. (Sie möchten nicht, dass zu viele gleichzeitig ausgeführt werden, da (a)URLSession
ohnehin nur so viele gleichzeitig ausgeführt werden können, sodass das Risiko besteht, dass letztere Anforderungen eine Zeitüberschreitung aufweisen, und (b) Auswirkungen auf den Speicher.) Mit dem oben genannten Verfahren wird ein kontrollierter Grad an Parallelität erreicht beim Anstehen vieler Anfragen.