Hintergrundverarbeitung mit OperationQueue
Main-Thread
Der Main-Thread einer iOS-App ist dafür zuständig, alle Ereignisse, wie Touch- und Zeichenevents, zu behandeln und sollte daher niemals mit länger dauernden Operationen, wie zum Beispiel Warten auf I/O oder Netzwerkzugriffe oder aufwendigen Berechnungen, blockiert werden. Solche Operationen sollten über Queues im Hintergrund, außerhalb des Main-Thread, ausgeführt werden.
Hintergrundverarbeitung mit OperationQueue
Mit einer OperationQueue können Operationen im Hintergrund abgearbeitet werden. So kann beispielsweise eine langlaufende Berechnung in den Hintergrund verlagert werden:
let backgroundQueue = OperationQueue()
backgroundQueue.addOperation {
let result = self.calculate()
}
Soll das Ergebnis einer solchen Hintergrundoperation im UI angezeigt werden, ist darauf zu achten, dass mit UI-Objekten nur vom Main Thread aus gearbeitet werden darf, da es ansonsten zu undefiniertem Verhalten kommt. Über OperationQueue.main können Operationen zur Abarbeitung durch die Main Queue eingestellt werden:
OperationQueue.main.addOperation {
label.text = "\(result)"
}
Diese beiden Operationen werden häufig miteinander verknüpft: im Hintergrund werden Daten beschafft oder berechnet - stehen diese zur Verfügung, stellt die Hintergrundoperation eine Operation in die Main Queue ein, um die Benutzeroberfläche zu aktualisieren:
backgroundQueue.addOperation {
let result = self.calculate()
OperationQueue.main.addOperation {
label.text = "\(result)"
}
}
Hintergrundverarbeitung mit DispatchQueue
Alternativ könnte auch die Klasse DispatchQueue aus dem Dispatch-Framework verwendet werden, die ebenfalls die Möglichkeit bereitstellt, Blöcke in Hintergrund- und Main-Queue einzustellen:
DispatchQueue.global(qos: .userInitiated).async {
let result = self.calculate()
DispatchQueue.main.async {
label.text = "\(result)"
}
}
Abhängigkeiten zwischen Operationen
Bei der OperationQueue ist insbesondere die Möglichkeit praktisch, sehr einfach Abhängigkeiten zwischen Operationen zu definieren, so dass Operationen auf das Ergebnis von anderen Operationen warten:
let op1 = BlockOperation() {
NSLog("Background operation 1")
}
let op2 = BlockOperation() {
NSLog("Background operation 2")
}
op2.addDependency(op1)
let queue = OperationQueue()
queue.addOperation(op1)
queue.addOperation(op2)
Tutorial: Code im Hintergrund ausführen mit OperationQueue
-
Laden Sie den Start-Stand von dem Beispielprojekt OperationQueueExample.
-
Mache Dich mit dem Beispielprojekt vertraut: Der BackgroundOperationViewController ruft die langläufige Funktion approximatePi auf. Diese blockiert den UI-Thread der Anwendung bis das Ergebnis berechnet ist, erkennbar daran das die Button-Animation für einige Zeit hängt.
-
Setze einen Haltepunkt auf die startCalculate-Methode: prüfe im Debug Navigator View, welcher Thread diese Methode ausführt:
-
Extrahiere mit dem Extract Method-Refactoring den Aufruf der approximatePi-Methode in eine neue Methode calculateInBackground:
-
Deklariere eine OperationQueue und verlagere den Aufruf mit addOperation auf einen Hintergrund-Thread. Es kann nun kein Wert mehr zurückgegeben werden - passe den Code zunächst so an, das das Ergebnis auf der Debug-Konsole ausgegeben wird:
class BackgroundOperationViewController: UIViewController { let backgroundQueue = OperationQueue() @IBOutlet var resultsLabel: UILabel! @IBOutlet var activityIndicator: UIActivityIndicatorView! private func calculateInBackground() { backgroundQueue.addOperation { debugPrint(approximatePi()) } } @IBAction func startCalculate() { calculateInBackground() // TODO: update UI: self.resultsLabel.text = String(...) } }
-
Starte die App und prüfe mit einem Haltepunkt, das der Code-Block, der addOperation übergeben wird, im Hintergrund ausgeführt wird.
-
Verwende einen completionHandler-Block um den Aufrufer nach der Berechnung des Ergebnisses zu informieren:
private func calculateInBackground(completionHandler: @escaping (_ result : Double) -> Void) { backgroundQueue.addOperation { completionHandler(approximatePi()) } } @IBAction func startCalculate() { calculateInBackground { (result) in self.resultsLabel.text = String(result) } }
-
Dies führt zu einem Laufzeitfehler UILabel.text must be used from main thread only, da jetzt UIKit-Objekte von einem Hintergrund-Thread aus verwendet werden:
-
Verlagere das Aktualisieren der UI wieder auf den Main-Thread:
@IBAction func startCalculate() { calculateInBackground { (result) in OperationQueue.main.addOperation { self.resultsLabel.text = String(result) } }
-
Ergänze Code für die Anzeige des Activity-Indicators:
@IBAction func startCalculate() { self.activityIndicator.isHidden = false calculateInBackground { (result) in OperationQueue.main.addOperation { self.resultsLabel.text = String(result) self.activityIndicator.isHidden = true } } }
Weitere Informationen
- OperationQueue Class Reference
-
Dispatch FrameworkDokumentation der DispatchQueue-Klasse und des Dispatch-Frameworks zur Parallelverarbeitung.