2016-10-14 4 views
3

Note: Während es andere, ähnliche Fragen zu diesem in SO gibt, scheinen in keinem von ihnen die Autoren den Lebenszyklus der Operation von selbst zu steuern. Bitte lesen Sie sich durch, bevor Sie sich auf eine andere Frage beziehen.Alamofire Anfrage läuft nicht Abschlussblock innerhalb NSOperation

Ich habe eine [NS] Operation in Swift 3.0 erstellt, um einige Daten in Core Data herunterzuladen, zu parsen und zwischenzuspeichern.

Zuerst habe ich die main() Methode in der Operation verwendet, um die Aufgabe auszuführen, und es hat gut funktioniert. Jetzt muss ich mehrere separate Aufgaben ausführen, um Informationen über jedes einzelne Gerät zu erhalten, das ich in diesem Schritt erhalten habe. Dafür muss ich sicherstellen, dass die Geräte tatsächlich in Core Data sind, bevor ich versuche, die anderen Informationen zu erhalten. Aus diesem Grund möchte ich sicherstellen, dass ich mich entscheide, wann die Aufgabe abgeschlossen ist - und wann ALLE Geräte sicher im Cache sind - bevor die abhängigen Anfragen ausgelöst werden.

Das Problem ist, dass, obwohl ich überprüft habe, dass Alamofire die Anfrage ausführt und der Server die Daten sendet, der Completion-Block mit dem Kommentar [THIS WONT EXECUTE!] nie ausgeführt wird. Dies führt dazu, dass die Warteschlange zum Stillstand kommt, da die Operation innerhalb des besagten Vervollständigungsblocks als finished markiert ist, was das gewünschte Verhalten ist.

Hat jemand irgendwelche Ideen, was hier vor sich geht?

class FetchDevices: Operation { 
var container: NSPersistentContainer! 
var alamofireManager: Alamofire.SessionManager! 
var host: String! 
var port: Int! 

private var _executing = false 
private var _finished = false 

override internal(set) var isExecuting: Bool { 
    get { 
     return _executing 
    } 

    set { 
     willChangeValue(forKey: "isExecuting") 
     _executing = newValue 
     didChangeValue(forKey: "isExecuting") 
    } 
} 

override internal(set) var isFinished: Bool { 
    get { 
     return _finished 
    } 

    set { 
     willChangeValue(forKey: "isFinished") 
     _finished = newValue 
     didChangeValue(forKey: "isFinished") 
    } 
} 

override var isAsynchronous: Bool { 
    return true 
} 

init(usingContainer container: NSPersistentContainer, usingHost host: String, usingPort port: Int) { 
    super.init() 

    self.container = container 
    self.host = host 
    self.port = port 

    let configuration = URLSessionConfiguration.default 
    configuration.timeoutIntervalForResource = 10 // in seconds 
    self.alamofireManager = Alamofire.SessionManager(configuration: configuration) 
} 

override func start() { 
    if self.isCancelled { 
     self.isFinished = true 
     return 
    } 

    self.isExecuting = true 

    alamofireManager!.request("http://apiurlfor.devices") 
     .validate() 
     .responseJSON { response in 
      // THIS WONT EXECUTE! 
      if self.isCancelled { 
       self.isExecuting = false 
       self.isFinished = true 
       return 
      } 

      switch response.result { 
      case .success(let value): 
       let jsonData = JSON(value) 

       self.container.performBackgroundTask { context in 
        for (_, rawDevice):(String, JSON) in jsonData { 
         let _ = Device(fromJSON: rawDevice, usingContext: context) 
        } 

        do { 
         try context.save() 
        } catch { 
         let saveError = error as NSError 
         print("\(saveError), \(saveError.userInfo)") 
        } 

        self.isExecuting = false 
        self.isFinished = true 
       } 

      case .failure(let error): 
       print("May Day! May Day! \(error)") 
       self.isExecuting = false 
       self.isFinished = true 
      } 
    } 
    } 
} 

Eine Information, die nützlich sein kann, ist, dass bei dem Verfahren, wo ich alle Operationen Warteschlange, I queue.waitUntilAllOperationsAreFinished() verwenden, um einen Abschluss-Handler auszuführen, nachdem alles getan ist.

Antwort

1

Das Problem ist, dass Sie etwas anderes haben, das den Hauptthread blockiert, der responseJSON für seine Schließung verwendet, was zu einem Deadlock führt. Sie können dies bestätigen, indem Sie schnell responseJSON durch .responseJSON(queue: .global()) ersetzen, damit Alamofire eine andere Warteschlange als die Hauptwarteschlange verwendet, und Sie werden sehen, dass sich dieses Verhalten ändert. Aber wenn du das tust (nur für Diagnosezwecke), solltest du es zurück ändern und dann deine Aufmerksamkeit darauf richten, was den Hauptthread blockiert (dh warte nicht auf den Hauptthread), wie du niemals den Haupt-Bedroung.


Sie erwähnen, dass Sie waitUntilAllOperationsAreFinished anrufen. Während dies eine berauschend einfache Lösung ist, um auf eine Reihe von Operationen zu warten, sollten Sie das nie aus dem Hauptthread tun. Der Haupt-Thread sollte niemals blockiert werden (oder zumindest nicht für mehr als ein paar Millisekunden). Dies kann zu einem UX mit unterdurchschnittlicher Qualität führen (bei dem die App nicht mehr reagiert), und Ihre App ist anfällig für die sofortige Beendigung durch den Prozess "Watchdog". Ich vermute, dass einer der Gründe, warum die Autor/innen von Alamofire sich so wohl gefühlt haben, ihre Completion-Handler standardmäßig in die Hauptwarteschlange zu schicken, ist, dass sie nicht nur oft nützlich und praktisch ist, sondern auch den Hauptthread blockiert.

Wenn der Betrieb Warteschlangen verwenden, das typische Muster warten, um zu vermeiden, immer ist eine Fertigstellung Betrieb zu verwenden:

let completionOperation = BlockOperation { 
    // something that we'll do when all the operations are done 
} 

for object in arrayOfObjects { 
    let networkOperation = ... 
    completionOperation.addDependency(networkOperation) 
    queue.addOperation(networkOperation) 
} 

OperationQueue.main.addOperation(completionOperation) 

Sie etwas Ähnliches erreichen können, wenn Sie Dispatch Gruppen und Versandgruppe „benachrichtigen“ (obwohl, verwenden in der Regel Wenn Sie Warteschlangen verwenden, bleiben Sie aus Gründen der Konsistenz in der Regel im Warteschlangenparadigma.

Wenn Sie waitUntilAllOperationsAreFinished aufrufen möchten, können Sie das technisch tun, aber Sie sollten nur tun, wenn Sie das "Warten" auf einige Hintergrundwarteschlange (z.eine globale Warteschlange, aber offensichtlich nicht zu Ihrer eigenen Operationswarteschlange, zu der Sie alle diese Operationen hinzugefügt haben). Ich denke jedoch, dass dies ein verschwenderisches Muster ist (warum binden Sie einen globalen Worker-Thread ein, der darauf wartet, dass Vorgänge beendet werden, wenn Sie einen vollkommen guten Mechanismus zum Festlegen eines Abschlussvorgangs haben).

+0

Vielen Dank, dass Sie sich die Zeit genommen haben, @Rob zu beantworten. Ich habe getan, was Sie vorgeschlagen haben, und es gab keine Veränderung. Ich werde mir die Sache mit dem "Blockieren des Hauptthreads" ansehen, aber ich sehe nicht, was das verursachen kann. Der Code, den Sie sehen, ist die ganze Operation. Es gibt nicht mehr dazu. – reydelleon

+0

OK @Rob, ich denke du hast in die richtige Richtung gewiesen, aber es gibt noch mehr zu tun. Ich benutze 'queue.waitUntilAllOperationsAreFinished()' in der Methode, die für das Einreihen der Aufgaben verantwortlich ist. Wenn ich das kommentiere, wird die Warteschlange nicht stehen bleiben, aber ich muss tatsächlich warten, bis alle Operationen beendet sind, bevor ich einen Beendigungshandler anrufe. Gibt es einen Weg dahin? – reydelleon

+0

Ihre Antwort hat mich am Ende in die richtige Richtung geschickt. Ich habe es gerade bearbeitet, um einen Link zu einer Frage hinzuzufügen, die das ursprüngliche Problem anspricht, das ich erst gefunden habe, nachdem ich das Problem unter Berücksichtigung der Zeiger analysiert hatte. – reydelleon