NSURLSession con NSBlockOperation y colas


Tengo una aplicación que actualmente usa NSURLConnection para la gran mayoría de sus redes. Me gustaría pasar a NSURLSession porque Apple me dice que ese es el camino a seguir.

Mi aplicación solo usa la versión síncrona de NSURLConnection mediante el método de clase + (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:(NSURLResponse **)response error:(NSError **)error. Hago esto dentro de un NSBlockOperation que se ejecuta en un NSOperationQueue por lo que no estoy bloqueando innecesariamente la cola principal. La gran ventaja de hacer las cosas de esta manera es que puedo hacer que las operaciones dependan unas de otras. Por ejemplo, puedo tener la tarea que es la solicitud de datos depende de la finalización de la tarea de inicio de sesión.

No he visto ningún soporte para operaciones síncronas dentro de NSURLSession. Todo lo que puedo encontrar son artículos que me ridiculizan por siquiera pensar en usarlo sincrónicamente y que soy una persona horrible por bloquear los hilos. Fino. Pero no veo manera de hacer que NSURLSessionTasks dependan el uno del otro. ¿Hay alguna manera de hacer eso?

¿O hay una descripción de cómo haría tal cosa de una manera diferente?

Author: Erik Allen, 2014-01-18

2 answers

Las críticas más duras a las solicitudes de red síncronas están reservadas para aquellos que lo hacen desde la cola principal (ya que sabemos que uno nunca debe bloquear la cola principal). Pero lo estás haciendo en tu propia cola de fondo, que aborda el problema más atroz con las solicitudes sincrónicas. Pero está perdiendo algunas características maravillosas que proporcionan las técnicas asíncronas (por ejemplo, cancelación de solicitudes, si es necesario).

Responderé a tu pregunta (cómo hacer que NSURLSessionDataTask se comporte sincrónicamente) a continuación, pero realmente te animo a abrazar los patrones asincrónicos en lugar de luchar contra ellos. Sugiero refactorizar tu código para usar patrones asíncronos. Específicamente, si una tarea depende de otra, simplemente ponga el inicio de la tarea dependiente en el controlador de finalización de la tarea anterior.

Si tienes problemas en esa conversión, entonces publica otra pregunta de Desbordamiento de pila, mostrándonos lo que intentaste, y podemos intentar ayudarte.


Si quieres para hacer que una operación asíncrona sea sincrónica, un patrón común es usar un semáforo de despacho para que el hilo que inició el proceso asíncrono pueda esperar una señal del bloque de finalización de la operación asíncrona antes de continuar. Nunca hagas esto desde la cola principal, pero si lo haces desde alguna cola de fondo, puede ser un patrón útil.

Puede crear un semáforo con:

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

Entonces puede tener el bloque de finalización del proceso asíncrono señale el semáforo con:

dispatch_semaphore_signal(semaphore);

Y luego puede tener el código fuera del bloque de finalización (pero aún en la cola de fondo, no en la cola principal) esperar esa señal:

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

Entonces, con NSURLSessionDataTask, juntando todo eso, eso podría verse como: {[14]]}

[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
]);

Con NSURLConnection (ahora obsoleto), tienes que saltar a través de algunos aros para iniciar solicitudes desde una cola en segundo plano, pero NSURLSession lo maneja con gracia.


Dicho esto, usando el bloque operaciones como esta significa que las operaciones no responderán a los eventos de cancelación (mientras se ejecutan, al menos). Así que generalmente evito esta técnica de semáforo con operaciones de bloque y simplemente envuelvo las tareas de datos en la subclase asíncrona NSOperation. Entonces disfrutas de los beneficios de las operaciones, pero también puedes hacerlas cancelables. Es más trabajo, pero un patrón mucho mejor.

Por ejemplo:

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

Y

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

Donde:

//
//  AsynchronousOperation.h
//

@import Foundation;

@interface AsynchronousOperation : NSOperation

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

- (void)completeOperation;

@end

Y

//
//  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
 105
Author: Rob,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-12-13 04:39:08

@Rob Le animo a publicar su respuesta como una solución, en vista de la siguiente nota de documentación de NSURLSession.dataTaskWithURL(_:completionHandler:):

Este método pretende ser una alternativa a la sendAsynchronousRequest: queue: completionHandler: método de NSURLConnection, con la capacidad adicional de admitir la personalización autenticación y cancelación.

 2
Author: Andrew Ebling,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-11-21 10:06:05