30. Juli 2019

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 1: Code im Hintergrund ausführen mit OperationQueue

  1. Laden Sie den Start-Stand von dem Beispielprojekt OperationQueueExample.

  2. 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.

  3. Setze einen Haltepunkt auf die startCalculate-Methode: prüfe im Debug Navigator View, welcher Thread diese Methode ausführt:

    Main Thread
  4. Extrahiere mit dem Extract Method-Refactoring den Aufruf der approximatePi-Methode in eine neue Methode calculateInBackground:

    Extract Method

  5. 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(...)
    
        }
    
    }
  6. Starte die App und prüfe mit einem Haltepunkt, das der Code-Block, der addOperation übergeben wird, im Hintergrund ausgeführt wird.

  7. 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)
    }
    
    }
  8. 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:

    Main Thread Fehler

  9. Verlagere das Aktualisieren der UI wieder auf den Main-Thread:

    @IBAction func startCalculate() {
        calculateInBackground { (result) in
        OperationQueue.main.addOperation {
        self.resultsLabel.text = String(result)
    }
    }
  10. 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
            }
        }
    }