使用 dispatch_semaphore 时 iOS 14 崩溃僵尸

Posted

技术标签:

【中文标题】使用 dispatch_semaphore 时 iOS 14 崩溃僵尸【英文标题】:iOS 14 crash zombie when use dispatch_semaphore 【发布时间】:2020-10-12 13:08:49 【问题描述】:

我处理了一些旧代码,它运行良好,但现在只在 ios 14 上崩溃

这里是演示

static NSData *DownloadWithRange(NSURL *URL, NSError *__autoreleasing *error) 
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:URL];
    request.timeoutInterval = 10.0;
    
    __block NSData *data = nil;
    __block dispatch_semaphore_t sema = dispatch_semaphore_create(0);
    NSURLSessionConfiguration *config = NSURLSessionConfiguration.ephemeralSessionConfiguration;
    NSURLSession *URLSession = [NSURLSession sessionWithConfiguration:config];
    NSURLSessionDataTask *task = [URLSession dataTaskWithRequest:request completionHandler:^(NSData * _Nullable taskData, NSURLResponse * _Nullable response, NSError * _Nullable taskError) 
        
        data = taskData;
        if (error)
            *error = taskError;
        dispatch_semaphore_signal(sema);
    ];
    [task resume];
    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

    return data;


- (IBAction)crashButton:(id)sender 
    
    NSURL *url  = [NSURL URLWithString:@"http://error"];
    
    NSError * error = nil;
    NSData *compressedData = DownloadWithRange(url, &error);
    
    NSLog(@"error is %@",error);

在DownloadWithRange返回之前,taskError内存(NSURLError)已经释放

在 ios 13 上,它不会崩溃

真的很奇怪

【问题讨论】:

【参考方案1】:

僵尸诊断让您知道在数据返回时自动释放对象正在被释放。您不应该在一个线程中实例化一个自动释放对象并尝试在一个单独的线程上使用一个池来管理它。正如the docs 所说:

自动释放池本质上与当前线程和范围相关联。

虽然问题在 iOS 14 中的表现可能有所不同,但我不认为这种模式是可以接受/谨慎的。


如果你打算使用这种模式(我不建议这样做;见下文),你可以通过在返回之前复制调用线程上的错误对象来解决这个问题:

static NSData *DownloadWithRange(NSURL *URL, NSError * __autoreleasing *error) 
    ...

    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

    if (error) 
        *error = [*error copy];
    

    return data;


FWIW,这种使用信号量使异步方法同步运行的技术通常被认为是一种反模式。而且你绝对不应该在主线程中使用这种模式。

我建议采用异步模式:

- (NSURLSessionTask *)dataTaskWithURL:(NSURL *)url completion:(void (^ _Nonnull)(NSData * _Nullable data, NSError * _Nullable error))completion 
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
    request.timeoutInterval = 10.0;

    NSURLSessionConfiguration *config = NSURLSessionConfiguration.ephemeralSessionConfiguration;
    NSURLSession *session = [NSURLSession sessionWithConfiguration:config];

    NSURLSessionDataTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) 
        dispatch_async(dispatch_get_main_queue(), ^
            completion(data, error);
        );
    ];
    [task resume];
    [session finishTasksAndInvalidate];
    return task;

[self dataTaskWithURL:url completion:^(NSData * _Nullable data, NSError * _Nullable error) 
    // use `data` and `error` here
];

// but not here

注意,除了采用异步完成块模式之外,还有一些其他的观察:

如果您要为每个请求创建一个新的NSURLSession,请确保使其无效,否则您会泄漏内存。

我正在返回 NSURLSessionTask,一些调用者可能想要它以防他们可能想要取消请求(例如,如果相关视图被关闭或必须生成新请求)。但如上所示,如果你不想使用这个NSURLSessionTask 引用,则不需要。

我将完成处理程序分派回主队列。这不是绝对必要的,但它通常是一种有用的便利。

【讨论】:

以上是关于使用 dispatch_semaphore 时 iOS 14 崩溃僵尸的主要内容,如果未能解决你的问题,请参考以下文章

等待 dispatch_semaphore 以等待许多异步任务完成的正确方法

dispatch_semaphore

使用GCD中的dispatch_semaphore(信号量)处理一个界面多个请求(把握AFNet网络请求完成的正确时机)

多线程 加锁

iOS各种锁的比较

iOS 多线程与线程安全