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

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

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

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

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

2 ответа

Решение

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

Я отвечу на ваш вопрос (как сделать 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

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

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

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

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.

Другие вопросы по тегам