自升级到 iOS10 后,带 NSRunLoop 的信号量无法正常工作

Semaphore with NSRunLoop not working since upgrade to iOS10

我在我的应用程序中使用官方 Ricoh Theta iOS SDK (Link) 连接到 360° Ricoh Theta 相机。 SDK使用多个HTTP请求来触发图像的捕获和从相机下载图像。

SDK 在内部使用信号量来同步请求,但自从升级到 iOS10 后,由于某种原因这似乎不再有效。根据开发者论坛的说法,这个问题是已知的,但 Ricoh 显然并不关心。

我将其缩小到 SDK 中的一个特定部分,其中 SDK 通过 RunLoop 每 0.5 秒发送一次请求来循环检查图像是否可用。

这发生在这个函数中:

/**
 * 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;
}

问题是即使在 getState 方法 (dispatch_semaphore_signal(_semaphore);) 中触发了信号量,也从未调用 NSLog(@"HERE");。通过调试器中的行,我肯定知道这一点。

/**  
  * 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]; }

我看到 iOS10 中的 NSRunLoop 有一些小的变化,这可能与我在这里面临的问题有关吗?我的另一种感觉是,也许 iOS 无法获得将调用 dispatch_semaphore_wait.

的新线程

在过去的几个小时里,我一直在为 table 苦苦思索,真的希望你们中的某个人能帮上忙!

这与信号量无关。

documentation for -[NSRunLoop run] 说:

Puts the receiver into a permanent loop, during which time it processes data from all attached input sources. […]

If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:. In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers.

Manually removing all known input sources and timers from the run loop is not a guarantee that the run loop will exit. macOS can install and remove additional input sources as needed to process requests targeted at the receiver’s thread. Those sources could therefore prevent the run loop from exiting.

If you want the run loop to terminate, you shouldn't use this method. Instead, use one of the other run methods and also check other arbitrary conditions of your own, in a loop. A simple example would be:

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

where shouldKeepRunning is set to NO somewhere else in the program.

请特别注意我加粗的句子。没有理由期望 -run: 方法中的任何代码在 [runLoop run] 调用之后都会被执行。

似乎最初的程序员希望使计时器无效会删除保持 运行 循环 运行ning 的最后一个输入,并且它将 return 从其 -run 方法。这种希望是不合理的。它在某些情况下在某些版本的 OS 上起作用的事实是不幸的偶然事件。 (不幸的是,如果它没有奏效,他们就会意识到自己的错误并找到不同的方法。)

如果您尝试使用 Apple 建议的解决方法,请注意它仅在输入源(而非计时器)将 shouldKeepRunning 设置为 false 时有效。这是因为,如文档所述,当计时器触发时,-runMode:beforeDate: 不一定是 return。因此,您需要使用输入源来触发退出。

您可以使用 NSPort 到 "poke" 运行 循环。您可以使用 -performSelector:onThread:withObject:waitUntilDone:,定位正在 运行 循环的线程。您不能进行跨线程 NSRunLoop 访问,但您可以通过 -getCFRunLoop 下降到线程安全的 CFRunLoop API。然后,您可以使用 CFRunLoopPerformBlock()CFRunLoopWakeUp() 到 运行 一个将 shouldKeepRunning 设置为 false 的块。 -run: 方法应该 CFRetain() CFRunLoop 然后调用 CFRunLoopWakeUp() 的代码应该 CFRelease() 它,只是为了确保它在需要的时候存在。否则,原始线程与 CFRunLoopWakeUp().

中的任何最终工作之间存在潜在的竞争条件

综上所述,您可以使用 GCD 调度计时器源而不是 NSTimer 来替换其中的大部分,然后停止使用 NSRunLoop 东西。这取决于 -getState: 是否真的需要 运行 在与 -run: 相同的线程上。使用当前代码,它在同一个线程上执行 运行。使用调度计时器源,它不会。仅使用部分代码很难说,但在我看来使用调度计时器源应该没有任何问题。

我进行了以下更改,它对我很有效。

- (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];}

希望这会对某人有所帮助。