1

Ich verwende das offizielle Ricoh Theta iOS SDK (Link) in meiner App, um eine 360 ​​° Ricoh Theta Kamera anzuschließen. Das SDK verwendet mehrere HTTP-Anforderungen, um die Erfassung von Bildern auszulösen und Bilder von der Kamera herunterzuladen.Semaphore mit NSRunLoop funktioniert nicht seit Upgrade auf iOS10

Intern verwendet das SDK Semaphoren zum Synchronisieren der Anfragen, aber seit dem Upgrade auf iOS 10 scheint dies aus irgendeinem Grund nicht mehr zu funktionieren. Laut den Entwicklerforen ist das Problem bekannt, Ricoh scheint das aber nicht wirklich zu interessieren.

Ich habe es auf ein bestimmtes Teil im SDK eingegrenzt, wo das SDK in einer Schleife prüft, ob ein Bild verfügbar ist, indem eine Anfrage alle 0,5 Sekunden über eine RunLoop gesendet wird.

Dies geschieht in dieser Funktion:

/** 
* Start status monitoring 
* @param command ID of command to be monitored 
* @return Status indicating completion or error 
*/ 
- (NSString*)run:(NSString*)command 
{ 
    // Create and keep HTTP session 
    NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration]; 
    _session= [NSURLSession sessionWithConfiguration:config]; 

    _commandId = command; 

    // Semaphore for synchronization (cannot be entered until signal is called) 
    _semaphore = dispatch_semaphore_create(0); 

    // Create and start timer 
    NSTimer *timer = [NSTimer timerWithTimeInterval:0.5f 
              target:self 
              selector:@selector(getState:) 
              userInfo:nil 
              repeats:YES]; 
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; 
    [runLoop addTimer:timer forMode:NSRunLoopCommonModes]; 
    [runLoop run]; 

    // Wait until signal is called 
    dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER); 
    NSLog(@"HERE"); 
    return _state; 
} 

Das Problem ist, dass NSLog(@"HERE"); nicht einmal genannt wird, obwohl die Semaphore in der getState Methode (dispatch_semaphore_signal(_semaphore);) ausgelöst wird. Ich weiß das sicher, indem ich durch die Zeilen im Debugger gehe.

/** 
    * Delegate called during each set period of time 
    * @param timer Timer 
    */ 
- (void)getState:(NSTimer*)timer { 
    // Create JSON data 
    NSDictionary *body = @{@"id": _commandId}; 

    // Set the request-body. 
    [_request setHTTPBody:[NSJSONSerialization dataWithJSONObject:body options:0 error:nil]]; 

    // Send the url-request. 
    NSURLSessionDataTask* task = 
    [_session dataTaskWithRequest:_request 
       completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 
        if (!error) { 
         NSArray* array = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil]; 
         _state = [array valueForKey:@"state"]; 
         _fileUri = [array valueForKeyPath:@"results.fileUri"]; 
         NSLog(@"result: %@", _state); 
        } else { 
         _state = @"error"; 
         NSLog(@"GetStorageInfo: received data is invalid."); 
        } 
        if (![_state isEqualToString:@"inProgress"]) { 
         [timer invalidate]; 
         dispatch_semaphore_signal(_semaphore); 


         // Stop timer 
         [timer invalidate]; 
        } 
       }]; 
    [task resume]; } 

Ich habe es gesehen haben einige kleinere Änderungen an der NSRunLoop in iOS 10, ist diese möglicherweise bezogen auf das, was ich hier gegenüber? Ein anderes Gefühl, das ich habe, ist, dass iOS vielleicht keinen neuen Thread bekommt, der dispatch_semaphore_wait ruft.

Ich habe in den letzten paar Stunden meinen Kopf gegen den Tisch geschlagen, wirklich gehofft, dass einer von euch hier draußen helfen kann!

Antwort

3

Das hat nichts mit dem Semaphor zu tun.

The documentation for -[NSRunLoop run] sagt:

Versetzt den Empfänger in eine permanente Schleife, während welcher Zeit es verarbeitet Daten von allen angeschlossenen Eingangsquellen. [...]

Wenn der Laufschleife keine Eingabequellen oder Timer zugeordnet sind, wird diese Methode sofort beendet; Andernfalls wird der Empfänger in NSDefaultRunLoopMode ausgeführt, indem wiederholt runMode:beforeDate: aufgerufen wird. In anderen Worten, diese Methode beginnt effektiv eine Endlosschleife, die verarbeitet Daten aus der Eingangsquelle und Timer der Laufschleife.

Das manuelle Entfernen aller bekannten Eingabequellen und Timer aus der Laufschleife ist keine Garantie dafür, dass die Laufschleife beendet wird. macOS kann installieren und entfernen zusätzliche Eingabequellen wie benötigt, um Anfragen gerichtet am Empfänger Thread zu verarbeiten. Diese Quellen könnten daher verhindern, dass dieRun-Schleife beendet wird.

Wenn Sie möchten, dass die Ausführungsschleife beendet wird, sollten Sie diese Methode nicht verwenden. Verwenden Sie stattdessen eine der anderen Run-Methoden und überprüfen Sie auch andere beliebige Bedingungen in einer Schleife. Ein einfaches Beispiel wäre sein:

BOOL shouldKeepRunning = YES; // global 
NSRunLoop *theRL = [NSRunLoop currentRunLoop]; 
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]); 

wo shouldKeepRunning-NO irgendwo anders im Programm festgelegt ist.

Achten Sie besonders auf die Sätze, die ich geschrieben habe. Es gibt keinen Grund zu erwarten, dass jemals ein Code in der -run: Methode nach dem [runLoop run] Aufruf ausgeführt wird.

Es scheint, als ob die ursprünglichen Programmierer gehofft hatten, dass das Ungültigmachen des Timers die letzte Eingabe entfernen würde, die die Laufschleife weiterlaufen ließ, und sie würde von ihrer -run Methode zurückkehren. Diese Hoffnung ist nicht gerechtfertigt. Die Tatsache, dass es in einigen Fällen auf einigen Versionen des Betriebssystems funktionierte, war ein unglücklicher Zufall. (Pech, denn wenn es nicht geklappt hätte, hätten sie ihren Fehler erkannt und einen anderen Ansatz gefunden.)

Wenn Sie versuchen, die von Apple vorgeschlagene Problemumgehung zu verwenden, werden Sie gewarnt, dass es nur funktioniert, wenn eine Eingabequelle (kein Timer) setzt shouldKeepRunning auf false. Das liegt daran, dass, wie dokumentiert, -runMode:beforeDate: nicht unbedingt zurückkehrt, wenn ein Timer ausgelöst wird. Sie müssen also eine Eingabequelle verwenden, um den Exit auszulösen.

Sie können eine NSPort auf „Poke“ die Laufschleife verwenden. Sie können -performSelector:onThread:withObject:waitUntilDone: verwenden und auf den Thread abzielen, der die Schleife ausführt. Sie können nicht cross-thread NSRunLoop zugreifen, aber Sie können auf die CFRunLoop API, die thread-safe ist, über -getCFRunLoop zugreifen. Dann können Sie CFRunLoopPerformBlock() und CFRunLoopWakeUp() verwenden, um einen Block auszuführen, der shouldKeepRunning auf false setzt. Die -run: Methode sollte CFRetain() die CFRunLoop und den Code dann die CFRunLoopWakeUp() nennt sollte es CFRelease(), nur sicherstellen, dass es so lange lebt, wie gebraucht. Andernfalls gibt es eine potentielle Wettlaufsituation zwischen dem Originalfaden und allen abschließenden Arbeiten innerhalb von CFRunLoopWakeUp().

All dies gesagt, können Sie möglicherweise eine Menge davon ersetzen, indem Sie eine GCD-Versand-Timer-Quelle anstelle einer NSTimer und dann aufhören zu verwenden NSRunLoop Zeug. Das hängt davon ab, ob -getState: wirklich auf demselben Thread wie -run: ausgeführt werden muss. Mit dem aktuellen Code läuft es auf demselben Thread. Mit einer Versand-Timer-Quelle würde es nicht funktionieren. Es ist ein bisschen schwer mit nur teilweise Code zu sagen, aber es sieht für mich so aus, als sollte es kein Problem mit der Dispatch-Timer-Quelle geben.

+0

Vielen Dank für Ihre sehr detaillierte Antwort! Ich schätze das wirklich! Ich fand dieses Gist, das Ihrem vorgeschlagenen Ansatz folgen würde (https://gist.github.com/maicki/7622108), dennoch frage ich mich jetzt, wie man es in das vorhandene SDK einfügt. Mit einem Dispatch-Timer könnte ich gezwungenermaßen einen Completion-Block verwenden, aber das SDK gibt derzeit den Wert zurück, nachdem ich für den Semaphor synchron gewartet habe. Gibt es eine Möglichkeit, diese beiden Ansätze zu kombinieren? –

+0

Sie würden immer noch den Semaphor verwenden. Es ist nur so, dass Sie den 'NSTimer' durch eine Dispatch-Timer-Quelle ersetzen und dann die Lauf-Schleife nicht ausführen müssen. –

+0

Macht Sinn! Du hast mir den Tag gemacht, ernsthaft kann ich dir nicht genug danken! –

0

Ich machte folgende Änderungen und es hat nur für mich funktioniert.

- (NSString*)run:(NSString*)command{ 
NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration]; 

_session= [NSURLSession sessionWithConfiguration:config]; 

_commandId = command; 

_semaphore = dispatch_semaphore_create(0); 

NSTimer *timer = [NSTimer timerWithTimeInterval:0.5f 
             target:self 
             selector:@selector(getState:) 
             userInfo:nil 
             repeats:YES]; 

NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; 

[runLoop addTimer:timer forMode:NSRunLoopCommonModes]; 

[runLoop run]; 

dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER); 

return _state; 
} 


- (void)getState:(NSTimer*)timer{ 
if ([_state isEqualToString:@"done"]) 
{ 
    dispatch_semaphore_signal(_semaphore); 
    [timer invalidate]; 
    return; 
} 
NSDictionary *body = @{@"id": _commandId}; 

[_request setHTTPBody:[NSJSONSerialization dataWithJSONObject:body options:0 error:nil]]; 

NSURLSessionDataTask* task = 
[_session dataTaskWithRequest:_request 
      completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { 
       if (!error) { 
        NSArray* array = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil]; 
        _state = [array valueForKey:@"state"]; 
        _fileUri = [array valueForKeyPath:@"results.fileUri"]; 
        NSLog(@"result: %@", _state); 
       } else { 
        _state = @"error"; 
       } 
       if (![_state isEqualToString:@"inProgress"]) { 

       } 
      }]; 
[task resume];} 

hoffe das wird jemandem helfen.

Verwandte Themen