NSURLSession с NSBlockOperation и очередями

У меня есть приложение, которое в настоящее время использует NSURLConnection для подавляющего большинства своих сетей. Я хотел бы перейти на NSURLSession, потому что Apple говорит мне, что это правильный путь.

Мое приложение просто использует синхронную версию NSURLConnection посредством метода класса + (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)error. Я делаю это в NSBlockOperation, работающем на NSOperationQueue, чтобы не блокировать без необходимости основную очередь. Большим преимуществом такого подхода является то, что я могу сделать операции зависимыми друг от друга. Например, я могу сделать так, чтобы задача, запрашивающая данные, зависела от завершения задачи входа в систему.

Я не видел поддержки синхронных операций в NSURLSession. Все, что я могу найти, это статьи, высмеивающие меня за то, что я даже думаю об использовании его синхронно, и что я ужасный человек из-за блокировки потоков. Отлично. Но я не вижу способа сделать NSURLSessionTasks зависимыми друг от друга. Есть ли способ сделать это?

Или есть описание того, как я бы сделал такую ​​вещь по-другому?


person Erik Allen    schedule 18.01.2014    source источник
comment
Есть одно место, где синхронная NSURLSession очень полезна, и это самый простой способ. При написании утилиты командной строки, которая выходит и взаимодействует с сетью, нет причин использовать асинхронность, если только вы не собираетесь выполнять несколько запросов одновременно. Чтобы сделать его синхронным, я добавляю вокруг него блокировку семафора. Без этого приложение завершится до того, как завершится запрос, потому что больше ничего (например, цикл выполнения GUI) не поддерживает приложение. Подавляющее большинство программистов iOS/OS/X этого не делают, так что эта тема не особо поднимается.   -  person Eric apRhys    schedule 03.05.2015
comment
Спасибо за это (и @Rob за ответ). Куда бы я ни посмотрел, все, что я вижу, это куча нянек, ноющих о том, что вы никогда не должны делать синхронные запросы, вместо того, чтобы отвечать на вопрос. Бывают случаи, когда вам нужно быть синхронным — в моем случае я имею дело со сторонней библиотекой, которая делает обратные вызовы для моего кода, который должен выполнять запросы URL-адресов и не возвращаться в библиотеку, пока запрос не завершится.   -  person Jeff Loughlin    schedule 18.09.2015
comment
Ответ Роба великолепен. Я также был разочарован количеством ответов, на которые я наткнулся, в которых говорилось, что просто используйте асинхронность. Я включил принятый ответ в Категорию iOS, если кто-то хочет, чтобы его было легко использовать, прямая замена NSURLConnection sendSynchronousRequest:.   -  person aroth    schedule 01.04.2016


Ответы (3)


Самая резкая критика синхронных сетевых запросов предназначена для тех, кто делает это из основной очереди (так как мы знаем, что нельзя блокировать основную очередь). Но вы делаете это в своей собственной фоновой очереди, которая решает самую вопиющую проблему с синхронными запросами. Но вы теряете некоторые замечательные функции, которые предоставляют асинхронные методы (например, отмену запросов, если это необходимо).

Я отвечу на ваш вопрос (как заставить NSURLSessionDataTask вести себя синхронно) ниже, но я действительно призываю вас принять асинхронные шаблоны, а не бороться с ними. Я бы предложил рефакторинг вашего кода для использования асинхронных шаблонов. В частности, если одна задача зависит от другой, просто поместите инициацию зависимой задачи в обработчик завершения предыдущей задачи.

Если у вас возникли проблемы с этим преобразованием, опубликуйте еще один вопрос о переполнении стека, показав нам, что вы пробовали, и мы можем попытаться вам помочь.


Если вы хотите сделать асинхронную операцию синхронной, распространенным шаблоном является использование семафора отправки, чтобы ваш поток, инициировавший асинхронный процесс, мог дождаться сигнала от блока завершения асинхронной операции, прежде чем продолжить. Никогда не делайте этого из основной очереди, но если вы делаете это из какой-то фоновой очереди, это может быть полезным шаблоном.

Вы можете создать семафор с помощью:

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

Затем вы можете заставить блок завершения асинхронного процесса сигнализировать семафору с помощью:

dispatch_semaphore_signal(semaphore);

Затем вы можете заставить код вне блока завершения (но все еще в фоновой очереди, а не в основной очереди) ждать этого сигнала:

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

Итак, с NSURLSessionDataTask, собрав все это вместе, это может выглядеть так:

[queue addOperationWithBlock:^{

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    NSURLSession *session = [NSURLSession sharedSession]; // or create your own session with your own NSURLSessionConfiguration
    NSURLSessionTask *task = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        if (data) {
            // do whatever you want with the data here
        } else {
            NSLog(@"error = %@", error);
        }

        dispatch_semaphore_signal(semaphore);
    }];
    [task resume];

    // but have the thread wait until the task is done

    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

    // now carry on with other stuff contingent upon what you did above
]);

С NSURLConnection (теперь устаревшим) вам придется пройти через некоторые обручи, чтобы инициировать запросы из фоновой очереди, но NSURLSession справляется с этим изящно.


Сказав это, использование блочных операций, подобных этому, означает, что операции не будут реагировать на события отмены (по крайней мере, пока они выполняются). Поэтому я обычно избегаю этой техники семафора с блочными операциями и просто оборачиваю задачи с данными в асинхронный подкласс NSOperation. Тогда вы сможете пользоваться преимуществами операций, но вы также можете сделать их отменяемыми. Это больше работы, но гораздо лучший образец.

Например:

//
//  DataTaskOperation.h
//
//  Created by Robert Ryan on 12/12/15.
//  Copyright © 2015 Robert Ryan. All rights reserved.
//

@import Foundation;
#import "AsynchronousOperation.h"

NS_ASSUME_NONNULL_BEGIN

@interface DataTaskOperation : AsynchronousOperation

/// Creates a operation that retrieves the contents of a URL based on the specified URL request object, and calls a handler upon completion.
///
/// @param  request                    A NSURLRequest object that provides the URL, cache policy, request type, body data or body stream, and so on.
/// @param  dataTaskCompletionHandler  The completion handler to call when the load request is complete. This handler is executed on the delegate queue. This completion handler takes the following parameters:
///
/// @returns                           The new session data operation.

- (instancetype)initWithRequest:(NSURLRequest *)request dataTaskCompletionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))dataTaskCompletionHandler;

/// Creates a operation that retrieves the contents of a URL based on the specified URL request object, and calls a handler upon completion.
///
/// @param  url                        A NSURL object that provides the URL, cache policy, request type, body data or body stream, and so on.
/// @param  dataTaskCompletionHandler  The completion handler to call when the load request is complete. This handler is executed on the delegate queue. This completion handler takes the following parameters:
///
/// @returns                           The new session data operation.

- (instancetype)initWithURL:(NSURL *)url dataTaskCompletionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))dataTaskCompletionHandler;

@end

NS_ASSUME_NONNULL_END

а также

//
//  DataTaskOperation.m
//
//  Created by Robert Ryan on 12/12/15.
//  Copyright © 2015 Robert Ryan. All rights reserved.
//

#import "DataTaskOperation.h"

@interface DataTaskOperation ()

@property (nonatomic, strong) NSURLRequest *request;
@property (nonatomic, weak) NSURLSessionTask *task;
@property (nonatomic, copy) void (^dataTaskCompletionHandler)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error);

@end

@implementation DataTaskOperation

- (instancetype)initWithRequest:(NSURLRequest *)request dataTaskCompletionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))dataTaskCompletionHandler {
    self = [super init];
    if (self) {
        self.request = request;
        self.dataTaskCompletionHandler = dataTaskCompletionHandler;
    }
    return self;
}

- (instancetype)initWithURL:(NSURL *)url dataTaskCompletionHandler:(void (^)(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error))dataTaskCompletionHandler {
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    return [self initWithRequest:request dataTaskCompletionHandler:dataTaskCompletionHandler];
}

- (void)main {
    NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:self.request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
        self.dataTaskCompletionHandler(data, response, error);
        [self completeOperation];
    }];

    [task resume];
    self.task = task;
}

- (void)completeOperation {
    self.dataTaskCompletionHandler = nil;
    [super completeOperation];
}

- (void)cancel {
    [self.task cancel];
    [super cancel];
}

@end

Где:

//
//  AsynchronousOperation.h
//

@import Foundation;

@interface AsynchronousOperation : NSOperation

/// Complete the asynchronous operation.
///
/// This also triggers the necessary KVO to support asynchronous operations.

- (void)completeOperation;

@end

А также

//
//  AsynchronousOperation.m
//

#import "AsynchronousOperation.h"

@interface AsynchronousOperation ()

@property (nonatomic, getter = isFinished, readwrite)  BOOL finished;
@property (nonatomic, getter = isExecuting, readwrite) BOOL executing;

@end

@implementation AsynchronousOperation

@synthesize finished  = _finished;
@synthesize executing = _executing;

- (instancetype)init {
    self = [super init];
    if (self) {
        _finished  = NO;
        _executing = NO;
    }
    return self;
}

- (void)start {
    if ([self isCancelled]) {
        self.finished = YES;
        return;
    }

    self.executing = YES;

    [self main];
}

- (void)completeOperation {
    self.executing = NO;
    self.finished  = YES;
}

#pragma mark - NSOperation methods

- (BOOL)isAsynchronous {
    return YES;
}

- (BOOL)isExecuting {
    @synchronized(self) {
        return _executing;
    }
}

- (BOOL)isFinished {
    @synchronized(self) {
        return _finished;
    }
}

- (void)setExecuting:(BOOL)executing {
    @synchronized(self) {
        if (_executing != executing) {
            [self willChangeValueForKey:@"isExecuting"];
            _executing = executing;
            [self didChangeValueForKey:@"isExecuting"];
        }
    }
}

- (void)setFinished:(BOOL)finished {
    @synchronized(self) {
        if (_finished != finished) {
            [self willChangeValueForKey:@"isFinished"];
            _finished = finished;
            [self didChangeValueForKey:@"isFinished"];
        }
    }
}

@end
person Rob    schedule 18.01.2014
comment
Метод, который вы показываете, выглядит многообещающе, но я решил прислушаться к вашему другому совету и потратил некоторое время на работу над асинхронностью. Моя самая большая борьба заключалась в попытке воспроизвести зависимости. В итоге я использовал NSCondition для имитации этой функции, но я не уверен, что реализовал ее правильно. Было бы неплохо, если бы NSURLSessionDataTask поддерживал NSOperation, чтобы я мог позвонить addDependency. :-) - person Erik Allen; 22.01.2014
comment
@ErikAllen Нет абсолютно ничего, что мешало бы вам обернуть NSURLSessionTask в параллельный подкласс NSOperation, а затем пользоваться зависимостями (и, если выполняется много одновременных запросов, контролировать степень параллелизма). Если вы просто хотите выполнить простую задачу входа в систему, запустите другую задачу, вы можете использовать исполнение dataTaskWithURL с completionHandler для задачи входа в систему, а внутри `completionHandler инициировать следующую задачу (задачи). - person Rob; 23.01.2014
comment
Это действительно отличный ответ из-за совета в первых парах абзацев. Остальное читать не пришлось. Я тоже за рефакторинг, если это возможно. - person Mazyod; 12.06.2015
comment
Если у меня есть 4 зависимых задачи, A -> B -> C -> D, было бы хорошей практикой перейти на 3 уровня с обработчиками завершения внутри обработчиков завершения внутри обработчиков завершения...? - person Arthur Thompson; 13.12.2015
comment
@ArthurThompson - Если бы у меня была такая вложенность, я мог бы предложить отдельные функции, имена которых указывают на их роль. Например. A может быть login, B может быть retrieveTableOfContents, C может быть retrieveDetails и D может быть retrieveImages. Затем каждая функция может просто вызывать следующую функцию в своем обработчике завершения, разрешая ужасную вложенность и делая ее гораздо более понятной. Другим подходом будет асинхронная операция, и тогда вы можете создать четыре операции и либо объявить зависимости между ними, либо просто добавить их в последовательную очередь. - person Rob; 13.12.2015
comment
если я нажму на API и получу ответ 401, что означает, что срок действия моего сеанса истек ... поэтому для этого я должен нажать API входа в систему, а затем API, который ранее выполнялся, будет снова поражен .... я должен сделать с nsurlsession ..как мы можем это выполнить? - person Diksha; 19.05.2016
comment
Потрясающе @Rob большое спасибо, работает как шарм! - person Matz; 24.08.2016

@Rob Я бы посоветовал вам опубликовать свой ответ в качестве решения, учитывая следующее примечание к документации от NSURLSession.dataTaskWithURL(_:completionHandler:):

Этот метод предназначен в качестве альтернативы методу sendAsynchronousRequest:queue:completionHandler: NSURLConnection с добавленной возможностью поддержки пользовательской проверки подлинности и отмены.

person Andrew Ebling    schedule 21.11.2014

Если подход на основе семафора не работает, попробуйте подход на основе опроса.

var reply = Data()
/// We need to make a session object.
/// This is key to make this work. This won't work with shared session.
let conf = URLSessionConfiguration.ephemeral
let sess = URLSession(configuration: conf)
let task = sess.dataTask(with: u) { data, _, _ in
    reply = data ?? Data()
}
task.resume()
while task.state != .completed {
    Thread.sleep(forTimeInterval: 0.1)
}
FileHandle.standardOutput.write(reply)

Подход, основанный на опросе, работает очень надежно, но эффективно ограничивает максимальную пропускную способность интервалом опроса. В этом примере это было ограничено 10 раз/сек.


Подход, основанный на семафорах, до сих пор работал хорошо, но начиная с эпохи Xcode 11 он ломается. (может только мне?)

Задача данных не завершается, если я жду семафоры. Если я жду семафора в другом потоке, задача завершается с ошибкой.

nw_connection_copy_protocol_metadata [C2] Client called nw_connection_copy_protocol_metadata on unconnected nw_connection error.

Кажется, что-то было изменено в реализации, так как Apple перемещает Network.framework.

person eonil    schedule 15.10.2019