Gestión de múltiples conexiones NSURLConnection asincrónicas


88

Tengo un montón de código repetido en mi clase que se parece a lo siguiente:

NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request
                                                              delegate:self];

El problema con las solicitudes asincrónicas es que cuando tienes varias solicitudes y tienes un delegado asignado para tratarlas a todas como una entidad, muchas ramificaciones y códigos desagradables comienzan a formularse:

¿Qué tipo de datos estamos recuperando? Si contiene esto, haz eso, de lo contrario haz otro. Creo que sería útil poder etiquetar estas solicitudes asincrónicas, como si pudieras etiquetar vistas con ID.

Tenía curiosidad por saber qué estrategia es más eficiente para administrar una clase que maneja múltiples solicitudes asincrónicas.

Respuestas:


77

Realizo un seguimiento de las respuestas en un CFMutableDictionaryRef codificado por NSURLConnection asociado a él. es decir:

connectionToInfoMapping =
    CFDictionaryCreateMutable(
        kCFAllocatorDefault,
        0,
        &kCFTypeDictionaryKeyCallBacks,
        &kCFTypeDictionaryValueCallBacks);

Puede parecer extraño usar esto en lugar de NSMutableDictionary, pero lo hago porque este CFDictionary solo conserva sus claves (NSURLConnection) mientras que NSDictionary copia sus claves (y NSURLConnection no admite la copia).

Una vez hecho esto:

CFDictionaryAddValue(
    connectionToInfoMapping,
    connection,
    [NSMutableDictionary
        dictionaryWithObject:[NSMutableData data]
        forKey:@"receivedData"]);

y ahora tengo un diccionario de datos "info" para cada conexión que puedo usar para rastrear información sobre la conexión y el diccionario "info" ya contiene un objeto de datos mutable que puedo usar para almacenar los datos de respuesta a medida que ingresan.

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    NSMutableDictionary *connectionInfo =
        CFDictionaryGetValue(connectionToInfoMapping, connection);
    [[connectionInfo objectForKey:@"receivedData"] appendData:data];
}

Dado que es posible que dos o más conexiones asincrónicas puedan ingresar a los métodos delegados a la vez, ¿hay algo específico que se deba hacer para garantizar el comportamiento correcto?
PlagueHammer

(He creado una nueva pregunta aquí haciendo esto: stackoverflow.com/questions/1192294/… )
PlagueHammer

3
Esto no es seguro para subprocesos si se llama al delegado desde varios subprocesos. Debe utilizar bloqueos de exclusión mutua para proteger las estructuras de datos. Una mejor solución es subclasificar NSURLConnection y agregar respuestas y referencias de datos como variables de instancia. Estoy proporcionando una respuesta más detallada explicando esto en la pregunta de Nocturne: stackoverflow.com/questions/1192294/…
James Wald

4
Aldi ... es seguro para subprocesos siempre que inicie todas las conexiones desde el mismo subproceso (lo que puede hacer fácilmente invocando su método de conexión de inicio usando performSelector: onThread: withObject: waitUntilDone :). Poner todas las conexiones en una NSOperationQueue tiene diferentes problemas si intenta iniciar más conexiones que el máximo de operaciones simultáneas de la cola (las operaciones se ponen en cola en lugar de ejecutarse simultáneamente). NSOperationQueue funciona bien para las operaciones vinculadas a la CPU, pero para las operaciones vinculadas a la red, es mejor utilizar un enfoque que no utilice un grupo de subprocesos de tamaño fijo.
Matt Gallagher

1
Solo quería compartir que para iOS 6.0 y superior, puede usar a en [NSMapTable weakToStrongObjectsMapTable]lugar de CFMutableDictionaryRefay ahorrar la molestia. Funcionó bien para mí.
Shay Aviv

19

Tengo un proyecto en el que tengo dos NSURLConnections distintas y quería usar el mismo delegado. Lo que hice fue crear dos propiedades en mi clase, una para cada conexión. Luego, en el método de delegado, verifico si de qué conexión es


- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    if (connection == self.savingConnection) {
        [self.savingReturnedData appendData:data];
    }
    else {
        [self.sharingReturnedData appendData:data];
    }
}

Esto también me permite cancelar una conexión específica por nombre cuando sea necesario.


tenga cuidado, esto es problemático ya que tendrá condiciones de carrera
adit

¿Cómo asignas los nombres (saveConnection y sharingReturnedData) para cada conexión en primer lugar?
jsherk

@adit, no, no hay condición de carrera inherente a este código. Tendría que ir bastante lejos de su camino con el código de creación de conexión para crear una condición de carrera
Mike Abdullah

su 'solución' es exactamente lo que la pregunta original busca evitar, citando desde arriba: '... un montón de código feo y ramificado comienza a formularse ...'
stefanB

1
@adit ¿Por qué esto conducirá a una condición de carrera? Es un concepto nuevo para mí.
guptron

16

Subclasificar NSURLConnection para contener los datos es limpio, menos código que algunas de las otras respuestas, es más flexible y requiere menos atención sobre la administración de referencias.

// DataURLConnection.h
#import <Foundation/Foundation.h>
@interface DataURLConnection : NSURLConnection
@property(nonatomic, strong) NSMutableData *data;
@end

// DataURLConnection.m
#import "DataURLConnection.h"
@implementation DataURLConnection
@synthesize data;
@end

Úselo como lo haría con NSURLConnection y acumule los datos en su propiedad de datos:

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    ((DataURLConnection *)connection).data = [[NSMutableData alloc] init];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [((DataURLConnection *)connection).data appendData:data];
}

Eso es.

Si desea ir más allá, puede agregar un bloque para que sirva como devolución de llamada con solo un par de líneas más de código:

// Add to DataURLConnection.h/.m
@property(nonatomic, copy) void (^onComplete)();

Configúrelo así:

DataURLConnection *con = [[DataURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
con.onComplete = ^{
    [self myMethod:con];
};
[con start];

e invocarlo cuando la carga finalice así:

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    ((DataURLConnection *)connection).onComplete();
}

Puede extender el bloque para aceptar parámetros o simplemente pasar DataURLConnection como un argumento al método que lo necesita dentro del bloque no-args como se muestra


Esta es una respuesta fantástica que funcionó muy bien para mi caso. ¡Muy simple y limpio!
jwarrent

8

ESTA NO ES UNA RESPUESTA NUEVA. POR FAVOR, DÉJAME MOSTRARLE CÓMO LO HICE

Para distinguir diferentes NSURLConnection dentro de los métodos delegados de la misma clase, uso NSMutableDictionary, para configurar y eliminar NSURLConnection, usando su (NSString *)descriptionclave.

El objeto que elegí setObject:forKeyes la URL única que se usa para iniciar NSURLRequestlos NSURLConnectionusos.

Una vez establecido NSURLConnection se evalúa en

-(void)connectionDidFinishLoading:(NSURLConnection *)connection, it can be removed from the dictionary.

// This variable must be able to be referenced from - (void)connectionDidFinishLoading:(NSURLConnection *)connection
NSMutableDictionary *connDictGET = [[NSMutableDictionary alloc] init];
//...//

// You can use any object that can be referenced from - (void)connectionDidFinishLoading:(NSURLConnection *)connection
[connDictGET setObject:anyObjectThatCanBeReferencedFrom forKey:[aConnectionInstanceJustInitiated description]];
//...//

// At the delegate method, evaluate if the passed connection is the specific one which needs to be handled differently
if ([[connDictGET objectForKey:[connection description]] isEqual:anyObjectThatCanBeReferencedFrom]) {
// Do specific work for connection //

}
//...//

// When the connection is no longer needed, use (NSString *)description as key to remove object
[connDictGET removeObjectForKey:[connection description]];

5

Un enfoque que he adoptado es no utilizar el mismo objeto que el delegado para cada conexión. En su lugar, creo una nueva instancia de mi clase de análisis para cada conexión que se activa y configuro el delegado en esa instancia.


Encapsulación mucho mejor con respecto a una conexión.
Kedar Paranjape


2

Normalmente creo una variedad de diccionarios. Cada diccionario tiene un poco de información de identificación, un objeto NSMutableData para almacenar la respuesta y la conexión en sí. Cuando se activa un método delegado de conexión, busco el diccionario de la conexión y lo manejo en consecuencia.


Ben, ¿estaría bien pedirte un fragmento de código de muestra? Estoy tratando de imaginarme cómo lo estás haciendo, pero no todo está ahí.
Coocoo4Cocoa

Ben en particular, ¿cómo buscas el diccionario? No puede tener un diccionario de diccionarios ya que NSURLConnection no implementa NSCopying (por lo que no se puede usar como clave).
Adam Ernst

Matt tiene una excelente solución a continuación usando CFMutableDictionary, pero yo uso una variedad de diccionarios. Una búsqueda requiere una iteración. No es el más eficiente, pero es lo suficientemente rápido.
Ben Gottlieb

2

Una opción es simplemente crear una subclase de NSURLConnection y agregar una etiqueta o un método similar. El diseño de NSURLConnection es intencionalmente muy básico, por lo que es perfectamente aceptable.

O quizás podría crear una clase MyURLConnectionController que sea responsable de crear y recopilar los datos de una conexión. Entonces solo tendría que informar a su objeto controlador principal una vez que finalice la carga.


2

en iOS5 y superior, puede usar el método de clase sendAsynchronousRequest:queue:completionHandler:

No es necesario realizar un seguimiento de las conexiones, ya que la respuesta regresa en el controlador de finalización.


1

Me gusta ASIHTTPRequest .


Realmente me gusta la implementación de 'bloques' en ASIHTTPRequest, es como los tipos internos anónimos en Java. Esto supera a todas las demás soluciones en términos de limpieza y organización del código.
Matt Lyons

1

Como se señaló en otras respuestas, debe almacenar connectionInfo en algún lugar y buscarlos por conexión.

El tipo de datos más natural para esto es NSMutableDictionary, pero no se puede aceptar NSURLConnectioncomo claves ya que las conexiones no se pueden copiar.

Otra opción para usar NSURLConnectionscomo claves NSMutableDictionaryes usar NSValue valueWithNonretainedObject]:

NSMutableDictionary* dict = [NSMutableDictionary dictionary];
NSValue *key = [NSValue valueWithNonretainedObject:aConnection]
/* store: */
[dict setObject:connInfo forKey:key];
/* lookup: */
[dict objectForKey:key];

0

Decidí subclasificar NSURLConnection y agregar una etiqueta, un delegado y un NSMutabaleData. Tengo una clase DataController que maneja toda la gestión de datos, incluidas las solicitudes. Creé un protocolo DataControllerDelegate, para que las vistas / objetos individuales puedan escuchar el DataController para averiguar cuándo finalizaron sus solicitudes y, si es necesario, cuánto se ha descargado o qué errores. La clase DataController puede usar la subclase NSURLConnection para iniciar una nueva solicitud y guardar al delegado que desea escuchar el DataController para saber cuándo ha finalizado la solicitud. Esta es mi solución de trabajo en XCode 4.5.2 e ios 6.

El archivo DataController.h que declara el protocolo DataControllerDelegate). El DataController también es un singleton:

@interface DataController : NSObject

@property (strong, nonatomic)NSManagedObjectContext *context;
@property (strong, nonatomic)NSString *accessToken;

+(DataController *)sharedDataController;

-(void)generateAccessTokenWith:(NSString *)email password:(NSString *)password delegate:(id)delegate;

@end

@protocol DataControllerDelegate <NSObject>

-(void)dataFailedtoLoadWithMessage:(NSString *)message;
-(void)dataFinishedLoading;

@end

Los métodos clave en el archivo DataController.m:

-(void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    NSURLConnectionWithDelegate *customConnection = (NSURLConnectionWithDelegate *)connection;
    NSLog(@"DidReceiveResponse from %@", customConnection.tag);
    [[customConnection receivedData] setLength:0];
}

-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    NSURLConnectionWithDelegate *customConnection = (NSURLConnectionWithDelegate *)connection;
    NSLog(@"DidReceiveData from %@", customConnection.tag);
    [customConnection.receivedData appendData:data];

}

-(void)connectionDidFinishLoading:(NSURLConnection *)connection {
    NSURLConnectionWithDelegate *customConnection = (NSURLConnectionWithDelegate *)connection;
    NSLog(@"connectionDidFinishLoading from %@", customConnection.tag);
    NSLog(@"Data: %@", customConnection.receivedData);
    [customConnection.dataDelegate dataFinishedLoading];
}

-(void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    NSURLConnectionWithDelegate *customConnection = (NSURLConnectionWithDelegate *)connection;
    NSLog(@"DidFailWithError with %@", customConnection.tag);
    NSLog(@"Error: %@", [error localizedDescription]);
    [customConnection.dataDelegate dataFailedtoLoadWithMessage:[error localizedDescription]];
}

Y para iniciar una solicitud: [[NSURLConnectionWithDelegate alloc] initWithRequest:request delegate:self startImmediately:YES tag:@"Login" dataDelegate:delegate];

NSURLConnectionWithDelegate.h: @protocol DataControllerDelegate;

@interface NSURLConnectionWithDelegate : NSURLConnection

@property (strong, nonatomic) NSString *tag;
@property id <DataControllerDelegate> dataDelegate;
@property (strong, nonatomic) NSMutableData *receivedData;

-(id)initWithRequest:(NSURLRequest *)request delegate:(id)delegate startImmediately:(BOOL)startImmediately tag:(NSString *)tag dataDelegate:(id)dataDelegate;

@end

Y NSURLConnectionWithDelegate.m:

#import "NSURLConnectionWithDelegate.h"

@implementation NSURLConnectionWithDelegate

-(id)initWithRequest:(NSURLRequest *)request delegate:(id)delegate startImmediately:(BOOL)startImmediately tag:(NSString *)tag dataDelegate:(id)dataDelegate {
    self = [super initWithRequest:request delegate:delegate startImmediately:startImmediately];
    if (self) {
        self.tag = tag;
        self.dataDelegate = dataDelegate;
        self.receivedData = [[NSMutableData alloc] init];
    }
    return self;
}

@end

0

Cada NSURLConnection tiene un atributo hash, puede discriminar todo por este atributo.

Por ejemplo, necesito mantener cierta información antes y después de la conexión, por lo que mi RequestManager tiene un NSMutableDictionary para hacer esto.

Un ejemplo:

// Make Request
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLConnection *c = [[NSURLConnection alloc] initWithRequest:request delegate:self];

// Append Stuffs 
NSMutableDictionary *myStuff = [[NSMutableDictionary alloc] init];
[myStuff setObject:@"obj" forKey:@"key"];
NSNumber *connectionKey = [NSNumber numberWithInt:c.hash];

[connectionDatas setObject:myStuff forKey:connectionKey];

[c start];

Después de la solicitud:

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    NSLog(@"Received %d bytes of data",[responseData length]);

    NSNumber *connectionKey = [NSNumber numberWithInt:connection.hash];

    NSMutableDictionary *myStuff = [[connectionDatas objectForKey:connectionKey]mutableCopy];
    [connectionDatas removeObjectForKey:connectionKey];
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.