Подтвердить что ты не робот

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

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

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

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

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

4b9b3361

Ответ 1

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

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

Ответ 2

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

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

Ответ 3

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

/// 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.