Gestione di più connessioni NSURLConnection asincrone


88

Ho un sacco di codice ripetuto nella mia classe che assomiglia al seguente:

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

Il problema con le richieste asincrone è quando si hanno varie richieste in corso e si ha un delegato assegnato per trattarle tutte come un'unica entità, un sacco di ramificazioni e codice brutto inizia a formulare:

Che tipo di dati stiamo recuperando? Se contiene questo, fallo, altrimenti fai altro. Sarebbe utile, credo, essere in grado di taggare queste richieste asincrone, un po 'come se fossi in grado di taggare le viste con ID.

Ero curioso di sapere quale strategia fosse più efficiente per la gestione di una classe che gestisce più richieste asincrone.

Risposte:


77

Tengo traccia delle risposte in un CFMutableDictionaryRef codificato dalla NSURLConnection ad esso associato. cioè:

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

Può sembrare strano usarlo al posto di NSMutableDictionary, ma lo faccio perché questo CFDictionary mantiene solo le sue chiavi (NSURLConnection) mentre NSDictionary copia le sue chiavi (e NSURLConnection non supporta la copia).

Fatto ciò:

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

e ora ho un dizionario "info" di dati per ogni connessione che posso usare per tenere traccia delle informazioni sulla connessione e il dizionario "info" contiene già un oggetto dati modificabile che posso usare per memorizzare i dati di risposta quando arrivano.

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

Poiché è possibile che due o più connessioni asincrone possano immettere i metodi delegato contemporaneamente, c'è qualcosa di specifico che sarebbe necessario fare per garantire un comportamento corretto?
Debajit

(Ho creare una nuova domanda qui a chiedere questo: stackoverflow.com/questions/1192294/... )
Debajit

3
Questo non è thread-safe se il delegato viene chiamato da più thread. È necessario utilizzare blocchi di esclusione reciproca per proteggere le strutture dei dati. Una soluzione migliore è creare sottoclassi NSURLConnection e aggiungere risposte e riferimenti ai dati come variabili di istanza. Fornisco una risposta più dettagliata spiegando questo alla domanda di Nocturne: stackoverflow.com/questions/1192294/…
James Wald

4
Aldi ... è thread-safe a condizione che tu avvii tutte le connessioni dallo stesso thread (cosa che puoi fare facilmente invocando il tuo metodo di connessione iniziale usando performSelector: onThread: withObject: waitUntilDone :). Mettere tutte le connessioni in una NSOperationQueue presenta diversi problemi se si tenta di avviare più connessioni rispetto al numero massimo di operazioni simultanee della coda (le operazioni vengono messe in coda invece di essere eseguite contemporaneamente). NSOperationQueue funziona bene per le operazioni associate alla CPU, ma per le operazioni legate alla rete, è meglio usare un approccio che non utilizza un pool di thread di dimensioni fisse.
Matt Gallagher

1
Volevo solo condividerlo per iOS 6.0 e versioni successive, puoi usare a [NSMapTable weakToStrongObjectsMapTable]invece di a CFMutableDictionaryRefe risparmiare il fastidio. Ha funzionato bene per me.
Shay Aviv

19

Ho un progetto in cui ho due connessioni NSURLC distinte e volevo utilizzare lo stesso delegato. Quello che ho fatto è stato creare due proprietà nella mia classe, una per ogni connessione. Quindi nel metodo delegato, controllo per vedere se si tratta di connessione


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

Ciò mi consente anche di annullare una connessione specifica per nome quando necessario.


attenzione, questo è problematico in quanto avrà le condizioni di gara
adit il

Come si assegnano i nomi (savingConnection e sharingReturnedData) per ogni connessione in primo luogo?
jsherk

@adit, no, non esiste una condizione di competizione inerente a questo codice. Dovresti andare abbastanza lontano con il codice di creazione della connessione per creare una condizione di gara
Mike Abdullah

la tua 'soluzione' è esattamente ciò che la domanda originale sta cercando di evitare, citando dall'alto: '... un sacco di ramificazioni e codice brutto inizia a formulare ...'
stefanB

1
@adit Perché questo porterà a una condizione di gara? È un nuovo concetto per me.
abc123

16

La creazione di sottoclassi NSURLConnection per contenere i dati è pulita, meno codice rispetto ad alcune delle altre risposte, è più flessibile e richiede meno attenzione alla gestione dei riferimenti.

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

Usalo come faresti con NSURLConnection e accumula i dati nella sua proprietà data:

- (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];
}

Questo è tutto.

Se vuoi andare oltre puoi aggiungere un blocco che funga da callback con solo un paio di righe di codice in più:

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

Impostalo in questo modo:

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

e invocalo quando il caricamento è finito in questo modo:

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

È possibile estendere il blocco per accettare i parametri o semplicemente passare DataURLConnection come argomento al metodo che ne ha bisogno all'interno del blocco no-args come mostrato


Questa è una risposta fantastica che ha funzionato davvero bene per il mio caso. Molto semplice e pulito!
jwarrent

8

QUESTA NON È UNA NUOVA RISPOSTA. PER FAVORE, FATEMI MOSTRARE COME HO FATTO

Per distinguere diversi NSURLConnection all'interno dei metodi delegati della stessa classe, utilizzo NSMutableDictionary, per impostare e rimuovere NSURLConnection, utilizzando la sua (NSString *)descriptionchiave.

L'oggetto che ho scelto setObject:forKeyè l'URL univoco utilizzato per l'avvio NSURLRequest, gli NSURLConnectionusi.

Una volta impostato NSURLConnection viene valutato in

-(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 approccio che ho adottato è quello di non utilizzare lo stesso oggetto del delegato per ogni connessione. Invece, creo una nuova istanza della mia classe di analisi per ogni connessione che viene attivata e imposto il delegato su quell'istanza.


Incapsulamento molto migliore rispetto a una connessione.
Kedar Paranjape


2

Di solito creo una serie di dizionari. Ogni dizionario ha un po 'di informazioni di identificazione, un oggetto NSMutableData per memorizzare la risposta e la connessione stessa. Quando viene attivato un metodo delegato di connessione, cerco il dizionario della connessione e lo gestisco di conseguenza.


Ben, sarebbe giusto chiederti un pezzo di codice di esempio? Sto cercando di immaginare come lo stai facendo, ma non è tutto lì.
Coocoo4Cocoa

In particolare Ben, come cerchi nel dizionario? Non puoi avere un dizionario di dizionari poiché NSURLConnection non implementa NSCopying (quindi non può essere usato come chiave).
Adam Ernst

Matt ha una soluzione eccellente di seguito utilizzando CFMutableDictionary, ma io uso una serie di dizionari. Una ricerca richiede un'iterazione. Non è il più efficiente, ma è abbastanza veloce.
Ben Gottlieb

2

Un'opzione è semplicemente creare una sottoclasse NSURLConnection e aggiungere un -tag o un metodo simile. Il design di NSURLConnection è intenzionalmente molto scarno, quindi questo è perfettamente accettabile.

O forse potresti creare una classe MyURLConnectionController responsabile della creazione e della raccolta dei dati di una connessione. Dovrebbe quindi solo informare il tuo oggetto controller principale una volta che il caricamento è terminato.


2

in iOS5 e versioni successive puoi semplicemente usare il metodo di classe sendAsynchronousRequest:queue:completionHandler:

Non è necessario tenere traccia delle connessioni poiché la risposta ritorna nel gestore di completamento.


1

Mi piace ASIHTTPRequest .


Mi piace molto l'implementazione dei "blocchi" in ASIHTTPRequest: è proprio come Anonymous Inner Types in Java. Questo batte tutte le altre soluzioni in termini di pulizia e organizzazione del codice.
Matt Lyons

1

Come sottolineato da altre risposte, dovresti memorizzare connectionInfo da qualche parte e cercarle tramite connessione.

Il tipo di dati più naturale per questo è NSMutableDictionary, ma non può accettare NSURLConnectioncome chiavi poiché le connessioni non sono copiabili.

Un'altra opzione per l'utilizzo NSURLConnectionscome chiavi in NSMutableDictionarysta utilizzando NSValue valueWithNonretainedObject]:

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

0

Ho deciso di creare una sottoclasse NSURLConnection e aggiungere un tag, delegate e NSMutabaleData. Ho una classe DataController che gestisce tutta la gestione dei dati, comprese le richieste. Ho creato un protocollo DataControllerDelegate, in modo che le singole viste / oggetti possano ascoltare il DataController per scoprire quando le loro richieste sono terminate e, se necessario, quanto è stato scaricato o errori. La classe DataController può utilizzare la sottoclasse NSURLConnection per avviare una nuova richiesta e salvare il delegato che desidera ascoltare il DataController per sapere quando la richiesta è terminata. Questa è la mia soluzione funzionante in XCode 4.5.2 e ios 6.

Il file DataController.h che dichiara il protocollo DataControllerDelegate). Il DataController è anche 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

I metodi chiave nel file 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]];
}

E per avviare una richiesta: [[NSURLConnectionWithDelegate alloc] initWithRequest:request delegate:self startImmediately:YES tag:@"Login" dataDelegate:delegate];

Il 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

E 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

Ogni NSURLConnection ha un attributo hash, puoi distinguere tutto da questo attributo.

Ad esempio, ho bisogno di mantenere determinate informazioni prima e dopo la connessione, quindi il mio RequestManager ha un NSMutableDictionary per farlo.

Un esempio:

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

Dopo richiesta:

- (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];
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.