NSRunLoop 在 iOS10 下冻结 iPad Pro 上的应用程序

NSRunLoop freezes app on iPad Pro under iOS10

我在自定义操作中收到同步请求,如下所示:

NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession* session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
NSURLSessionDataTask* task = [session dataTaskWithRequest:request completionHandler:^(NSData *_data, NSURLResponse *_response, NSError *_error)
                              {
                                  self->data = [_data retain];
                                  self->tempResponse = _response;
                                  self->tempError = [_error retain];
                                  self->done = true;
                              }];

[task resume];

// wait until the connection has finished downloading the data or the operation gets cancelled
while (!self->done && !self.isCancelled)
{
    [[NSRunLoop currentRunLoop] run];
}

这段代码很旧,但已经通过了几个 iOS 版本(仅在某些时候替换了 NSURLConnection)。现在,在 iPad Pro 上的 iOS 10 下,此代码将冻结我的应用程序。如果我将应用程序置于后台并重新打开它,它将再次 运行。此外,如果我在 [task resume]self->data = [_data retain]; 上放置断点,则根本不会发生冻结。

我找到了一种在代码中修复它的方法,即在 运行 循环中添加一个 NSLog:

while (!self->done && !self.isCancelled)
{
    NSLog(@"BLAH!");
    [[NSRunLoop currentRunLoop] run];
}

这会消耗相当多的性能,并且在很长一段时间内无济于事 运行,因为发布配置中删除了所有 NSLog。

所以我需要一种方法来修复这个错误。也许代码太旧了,有一种新的方法可以做到,我不知道。感谢任何帮助。

-运行 方法将永远有效,直到所有计时器等都从 运行 循环中删除。文档指出:

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.

因此,iOS 10 可能正在添加另一个输入源,该输入源仅在设备处于后台或类似情况时才被删除。或者,如果没有计时器或源,-运行 方法可能总是在之前立即返回(但使用大量 CPU 将是一个繁忙的等待)。使用该方法您确实没有太多控制权,可能还有其他一些方法。

我在测试用例代码中使用了以下 NSRunLoop 类别方法,但我不确定我是否会推荐它用于生产,因为它会大量使用 CPU,尽管可能会更改以检查每个 . 1 秒(或更长)会有所帮助,而不是使用 [NSDate date],这是一个 0 间隔。

- (BOOL)runWithTimeout:(NSTimeInterval)timeout untilCondition:(BOOL (^)(void))testBlock
{
    NSDate *limitDate = [NSDate dateWithTimeIntervalSinceNow:timeout];

    while (1)
    {
        if (testBlock())
            return YES;
        if ([NSDate timeIntervalSinceReferenceDate] > [limitDate timeIntervalSinceReferenceDate])
            return NO;
        if (![self runMode:NSDefaultRunLoopMode beforeDate:[NSDate date]])
            return NO;
    }
}

使用 NSURLSession 进行同步调用(我假设是在子线程中)的更常用方法是使用信号量:

dispatch_semaphore_t sem = dispatch_semaphore_create(0);  
NSURLSessionDataTask* task = [session dataTaskWithRequest:request completionHandler:^(NSData *_data, NSURLResponse *_response, NSError *_error)
                          {
                              self->data = [_data retain];
                              self->tempResponse = _response;
                              self->tempError = [_error retain];
                              dispatch_semaphore_signal(sem);  
                          }];

[task resume];
dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);  

由于还需要检查已取消,这可能会变得复杂——但您可以将信号量设为实例变量,并覆盖 -cancel 方法以同时调用信号量。确保您不再使用该信号量实例,因为您可能有多个信号(并且您可能需要检查网络完成块中的 self.isCancelled 以确保如果之前取消,您不会做任何额外的工作) .