iOS 14 crash zombie when use dispatch_semaphore

632 Views Asked by At

I handle some old code, it runs well, but now crash only on ios 14

here is the demo

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);
}

before DownloadWithRange returned, the taskError memory(NSURLError) has released

on ios 13, it don't crash

it's really weird

1

There are 1 best solutions below

0
On

The zombie diagnostics are letting you know that the autorelease object is getting deallocated by the time the data is returned. You should not be instantiating an autorelease object in one thread and trying to have a pool on a separate thread manage that. As the docs say:

Autorelease pools are tied to the current thread and scope by their nature.

While the problem might be manifesting itself differently in iOS 14, I do not believe that this pattern was ever acceptable/prudent.


If you're going to use this pattern (which I wouldn't advise; see below), you can solve this problem by copying the error object on the calling thread before returning:

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

    dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

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

    return data;
}

FWIW, this technique of using semaphore to make asynchronous method behave synchronously is generally considered an anti-pattern. And you definitely should never use this pattern from the main thread.

I would suggest adopting asynchronous patterns:

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

And

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

// but not here

Note, in addition to adopting asynchronous completion block pattern, a few other observations:

  • If you’re going to create a new NSURLSession for each request, make sure to invalidate it or else you will leak memory.

  • I’m returning the NSURLSessionTask, which some callers may want in case they might want to cancel the request (e.g. if the view in question is dismissed or a new request must be generated). But as shown above, you don’t need to use this NSURLSessionTask reference if you don’t want.

  • I'm dispatching the completion handler back to the main queue. That is not strictly necessary, but it is often a useful convenience.