Сбой NSURLSessionDownloadTask в NSOperation при отмене

#objective-c #macos

Вопрос:

Я пытаюсь создать подкласс DownloadOperation для NSOperation для асинхронной загрузки данных. Казалось, все работало нормально, пока я не попытался добавить поддержку отмены. По сути, обработчик завершения NSURLSessionDownloadTask операции, похоже, вызывается после того, как операция была выпущена. Произойдет сбой с EXC_BAD_ACCESS в строке weakSelf.state = kFinished .

Полный пример проекта здесь: https://github.com/angstsmurf/DownloadOperationQueue . Нажмите Command . после запуска сбой.

 #import "DownloadOperation.h"

typedef enum OperationState : NSUInteger {
    kReady,
    kExecuting,
    kFinished
} OperationState;

@interface DownloadOperation ()

@property NSURLSessionDownloadTask *task;

@property OperationState state;

@end


@implementation DownloadOperation


// default state is ready (when the operation is created)

@synthesize state = _state;

- (void)setState:(OperationState)state {
    @synchronized(self) {
        if (_state != state) {
            [self willChangeValueForKey:@"isExecuting"];
            [self willChangeValueForKey:@"isFinished"];
            _state = state;
            [self didChangeValueForKey: @"isExecuting"];
            [self didChangeValueForKey: @"isFinished"];
        }
    }
}

- (OperationState)state {
    @synchronized (self) {
        return _state;
    }
}

- (BOOL)isReady { return (self.state == kReady); }
- (BOOL)isExecuting { return (self.state  == kExecuting); }
- (BOOL)isFinished { return (self.state  == kFinished); }

- (BOOL)isAsynchronous {
    return YES;
}

- (instancetype)initWithSession:(NSURLSession *)session downloadTaskURL:(NSURL *)downloadTaskURL completionHandler:(nullable void (^)(NSURL * _Nullable,  NSURLResponse * _Nullable,  NSError * _Nullable))completionHandler {

    self = [super init];

    if (self) {
        __unsafe_unretained DownloadOperation *weakSelf = self;
        // use weak self to prevent retain cycle
        _task = [[NSURLSession sharedSession] downloadTaskWithURL:downloadTaskURL
                                                completionHandler:^(NSURL * _Nullable localURL, NSURLResponse * _Nullable response, NSError * _Nullable error) {

            /*
             if there is a custom completionHandler defined,
             pass the result gotten in downloadTask's completionHandler to the
             custom completionHandler
             */
            if (completionHandler) {
                completionHandler(localURL, response, error);
            }

            /*
             set the operation state to finished once
             the download task is completed or have error
             */
            weakSelf.state = kFinished;
        }];
    }

    return self;
}

- (void)start {
    /*
     if the operation or queue got cancelled even
     before the operation has started, set the
     operation state to finished and return
     */
    if (self.cancelled) {
        self.state = kFinished;
        return;
    }

    // set the state to executing
    self.state = kExecuting;

    NSLog(@"downloading %@", self.task.originalRequest.URL.absoluteString);

    // start the downloading
    [self.task resume];
}

-(void)cancel {
    [super cancel];

    // cancel the downloading
    [self.task cancel];
}

@end
 

Комментарии:

1. Вы записываете self as __unsafe_unretained , и именно поэтому в этом имени есть «небезопасный». Ваш DownloadOperation , по-видимому, удален до завершения загрузки. Вы можете либо 1) убедиться, что он живет достаточно долго downloadTaskWithURL для завершения, либо 2) захватить self в качестве слабой ссылки и проверить, равен ли он нулю, прежде чем использовать его.

2. Большое спасибо! Я попытался прочитать weak versus unsafe_unretained , но я не могу точно сказать, есть ли когда-либо какая-либо причина использовать последнее больше, если вы не нацелены на действительно старую систему. Большая часть документации, которую вы найдете в Интернете, так или иначе устарела.

3. При weak использовании ссылки, когда объект, на который она ссылается, удаляется, weak ссылка будет установлена на nil . Затем вы можете проверить ссылку (например, в вашем блоке), чтобы убедиться, что это nil так, прежде чем использовать ее. Использование unsafe_unretained указателя сохранит его значение, даже если объект будет удален. Использование weak ссылки приводит к небольшому снижению производительности при удалении объекта. Если вам все равно, будет ли объект удален из-под вас, и вы не хотите weak снижения производительности ссылки, вы можете использовать unsafe_unretained .

4. Для получения более современной документации (написанной в контексте Swift) вы можете посмотреть: Автоматический подсчет ссылок . Просто поймите, что в Swift unsafe_unretained вызывается unowned

5. весь пример забавный, потому что он имитирует то, что уже предлагает completionHandler.

Ответ №1:

Как указано в комментариях Скотта Томпсона, правильным ключевым словом для переменной weakSelf является __weak , not __unsafe_unretained .