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.

Coocoo4Cocoa
fuente

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];
}
Matt Gallagher
fuente
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.

jbarnhart
fuente
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

Pat Niemeyer
fuente
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]];
petershine
fuente
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.

Brad el chico de la aplicación
fuente
Encapsulación mucho mejor con respecto a una conexión.
Kedar Paranjape
4

Prueba mi clase personalizada, MultipleDownload , que se encarga de todo esto por ti.

Leonho
fuente
en iOS6 no puede usar NSURLConnection como clave.
user501836
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 Gottlieb
fuente
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.

Mike Abdullah
fuente
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.

Yariv Nissim
fuente
1

Me gusta ASIHTTPRequest .

ruipacheco
fuente
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];
mfazekas
fuente
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
Chris Slade
fuente
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];
}
viejo
fuente