Gestion de plusieurs connexions NSURLConnection asynchrones

88

J'ai une tonne de code répétitif dans ma classe qui ressemble à ceci:

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

Le problème avec les demandes asynchrones est que lorsque vous avez diverses demandes en cours et qu'un délégué est assigné pour les traiter toutes comme une seule entité, beaucoup de branches et de code laid commencent à se formuler:

Quel genre de données récupérons-nous? S'il contient ceci, faites cela, sinon faites autre. Il serait utile, je pense, de pouvoir baliser ces demandes asynchrones, un peu comme si vous pouviez baliser des vues avec des identifiants.

J'étais curieux de savoir quelle stratégie est la plus efficace pour gérer une classe qui gère plusieurs requêtes asynchrones.

Coocoo4Cocoa
la source

Réponses:

77

Je trace les réponses dans un CFMutableDictionaryRef saisi par le NSURLConnection qui lui est associé. c'est à dire:

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

Cela peut sembler étrange d'utiliser ceci au lieu de NSMutableDictionary mais je le fais parce que ce CFDictionary ne conserve que ses clés (le NSURLConnection) tandis que NSDictionary copie ses clés (et NSURLConnection ne prend pas en charge la copie).

Une fois que c'est fait:

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

et maintenant j'ai un dictionnaire "info" de données pour chaque connexion que je peux utiliser pour suivre les informations sur la connexion et le dictionnaire "info" contient déjà un objet de données mutable que je peux utiliser pour stocker les données de réponse au fur et à mesure qu'elles arrivent.

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    NSMutableDictionary *connectionInfo =
        CFDictionaryGetValue(connectionToInfoMapping, connection);
    [[connectionInfo objectForKey:@"receivedData"] appendData:data];
}
Matt Gallagher
la source
Puisqu'il est possible que deux ou plusieurs connexions asynchrones puissent entrer dans les méthodes de délégué à la fois, y a-t-il quelque chose de spécifique à faire pour garantir un comportement correct?
PlagueHammer
(J'ai créé une nouvelle question ici demandant ceci: stackoverflow.com/questions/1192294/… )
PlagueHammer
3
Ce n'est pas thread-safe si le délégué est appelé à partir de plusieurs threads. Vous devez utiliser des verrous d'exclusion mutuelle pour protéger les structures de données. Une meilleure solution consiste à sous-classer NSURLConnection et à ajouter des références de réponse et de données en tant que variables d'instance. Je fournis une réponse plus détaillée expliquant cela à la question de Nocturne: stackoverflow.com/questions/1192294/…
James Wald
4
Aldi ... il est thread-safe à condition que vous démarriez toutes les connexions à partir du même thread (ce que vous pouvez faire facilement en appelant votre méthode de connexion de démarrage en utilisant performSelector: onThread: withObject: waitUntilDone :). Placer toutes les connexions dans une NSOperationQueue pose des problèmes différents si vous essayez de démarrer plus de connexions que le nombre maximal d'opérations simultanées de la file d'attente (les opérations sont mises en file d'attente au lieu d'être exécutées simultanément). NSOperationQueue fonctionne bien pour les opérations liées au processeur, mais pour les opérations liées au réseau, il vaut mieux utiliser une approche qui n'utilise pas de pool de threads de taille fixe.
Matt Gallagher
1
Je voulais juste partager cela pour iOS 6.0 et au-dessus, vous pouvez utiliser un [NSMapTable weakToStrongObjectsMapTable]au lieu d'un CFMutableDictionaryRefet économiser les tracas. A bien fonctionné pour moi.
Shay Aviv
19

J'ai un projet où j'ai deux NSURLConnections distinctes, et je voulais utiliser le même délégué. Ce que j'ai fait, c'est créer deux propriétés dans ma classe, une pour chaque connexion. Ensuite, dans la méthode déléguée, je vérifie si de quelle connexion il s'agit


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

Cela me permet également d'annuler une connexion spécifique par nom en cas de besoin.

Jbarnhart
la source
attention ce qui est problématique , car il aura des conditions de course
ADIT
Comment attribuez-vous les noms (saveConnection et sharingReturnedData) pour chaque connexion en premier lieu?
jsherk
@adit, non, il n'y a pas de condition de concurrence inhérente à ce code. Vous auriez à aller assez loin de votre chemin avec le code de création de connexion pour créer une condition de
concurrence
votre `` solution '' est exactement ce que la question initiale cherche à éviter, citant ci-dessus: `` ... beaucoup de code de branchement et laid commence à se formuler ... ''
stefanB
1
@adit Pourquoi cela conduira-t-il à une condition de concurrence? C'est un nouveau concept pour moi.
guptron
16

Le sous-classement de NSURLConnection pour contenir les données est propre, moins de code que certaines des autres réponses, est plus flexible et nécessite moins de réflexion sur la gestion des références.

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

Utilisez-le comme vous le feriez pour NSURLConnection et accumulez les données dans sa propriété 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];
}

C'est tout.

Si vous souhaitez aller plus loin, vous pouvez ajouter un bloc pour servir de rappel avec juste quelques lignes de code supplémentaires:

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

Réglez-le comme ceci:

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

et invoquez-le lorsque le chargement est terminé comme ceci:

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

Vous pouvez étendre le bloc pour accepter les paramètres ou simplement passer le DataURLConnection comme argument à la méthode qui en a besoin dans le bloc no-args comme indiqué

Pat Niemeyer
la source
C'est une réponse fantastique qui a très bien fonctionné pour mon cas. Très simple et propre!
jwarrent
8

CE N'EST PAS UNE NOUVELLE RÉPONSE. VEUILLEZ ME MONTRER COMMENT J'AI FAIT

Pour distinguer différents NSURLConnection dans les méthodes déléguées de la même classe, j'utilise NSMutableDictionary, pour définir et supprimer NSURLConnection, en utilisant sa (NSString *)descriptionclé.

L'objet que j'ai choisi pour setObject:forKey est l'URL unique qui est utilisée pour l'initiation NSURLRequest, les NSURLConnectionutilisations.

Une fois défini, NSURLConnection est évalué à

-(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]];
Pétershine
la source
5

Une approche que j'ai adoptée consiste à ne pas utiliser le même objet que le délégué pour chaque connexion. Au lieu de cela, je crée une nouvelle instance de ma classe d'analyse pour chaque connexion qui est déclenchée et définit le délégué sur cette instance.

Brad The App Guy
la source
Encapsulation bien meilleure par rapport à une connexion.
Kedar Paranjape
4

Essayez ma classe personnalisée, MultipleDownload , qui gère tout cela pour vous.

Leonho
la source
sur iOS6 ne peut pas utiliser NSURLConnection comme clé.
user501836
2

Je crée généralement une gamme de dictionnaires. Chaque dictionnaire a un peu d'informations d'identification, un objet NSMutableData pour stocker la réponse et la connexion elle-même. Lorsqu'une méthode de délégué de connexion se déclenche, je recherche le dictionnaire de la connexion et le gère en conséquence.

Ben Gottlieb
la source
Ben, serait-il normal de vous demander un exemple de code? J'essaie d'imaginer comment vous le faites, mais tout n'est pas là.
Coocoo4Cocoa
En particulier Ben, comment recherchez-vous le dictionnaire? Vous ne pouvez pas avoir de dictionnaire de dictionnaires car NSURLConnection n'implémente pas NSCopying (il ne peut donc pas être utilisé comme clé).
Adam Ernst
Matt a une excellente solution ci-dessous en utilisant CFMutableDictionary, mais j'utilise un éventail de dictionnaires. Une recherche nécessite une itération. Ce n'est pas le plus efficace, mais c'est assez rapide.
Ben Gottlieb
2

Une option consiste simplement à sous-classer NSURLConnection vous-même et à ajouter une méthode -tag ou similaire. La conception de NSURLConnection est intentionnellement très simple, c'est donc parfaitement acceptable.

Ou peut-être pourriez-vous créer une classe MyURLConnectionController qui est responsable de la création et de la collecte des données d'une connexion. Il n'aurait alors qu'à informer votre objet contrôleur principal une fois le chargement terminé.

Mike Abdullah
la source
2

dans iOS5 et au-dessus, vous pouvez simplement utiliser la méthode de classe sendAsynchronousRequest:queue:completionHandler:

Pas besoin de garder une trace des connexions puisque la réponse revient dans le gestionnaire d'achèvement.

Yariv Nissim
la source
1

J'aime ASIHTTPRequest .

Ruipacheco
la source
J'aime vraiment l'implémentation des «blocs» dans ASIHTTPRequest - c'est comme les Anonymes Inner Types en Java. Cela surpasse toutes les autres solutions en termes de propreté et d'organisation du code.
Matt Lyons
1

Comme indiqué par d'autres réponses, vous devez stocker connectionInfo quelque part et les rechercher par connexion.

Le type de données le plus naturel pour cela est NSMutableDictionary, mais il ne peut pas accepterNSURLConnection comme clé car les connexions ne sont pas copiables.

Une autre option pour utiliser NSURLConnectionscomme clés dans NSMutableDictionaryconsiste à utiliser NSValue valueWithNonretainedObject]:

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

J'ai décidé de sous-classer NSURLConnection et d'ajouter une balise, un délégué et un NSMutabaleData. J'ai une classe DataController qui gère toute la gestion des données, y compris les demandes. J'ai créé un protocole DataControllerDelegate, afin que les vues / objets individuels puissent écouter le DataController pour savoir quand leurs demandes ont été terminées et, si nécessaire, combien a été téléchargé ou quelles erreurs. La classe DataController peut utiliser la sous-classe NSURLConnection pour démarrer une nouvelle demande et enregistrer le délégué qui souhaite écouter le DataController pour savoir quand la demande est terminée. C'est ma solution de travail dans XCode 4.5.2 et ios 6.

Le fichier DataController.h qui déclare le protocole DataControllerDelegate). Le DataController est également 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

Les méthodes clés dans le fichier 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]];
}

Et pour lancer une requête: [[NSURLConnectionWithDelegate alloc] initWithRequest:request delegate:self startImmediately:YES tag:@"Login" dataDelegate:delegate];

Le 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

Et le 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
la source
0

Chaque NSURLConnection a un attribut de hachage, vous pouvez tout discriminer par cet attribut.

Par exemple, j'ai besoin de conserver certaines informations avant et après la connexion, donc mon RequestManager a un NSMutableDictionary pour le faire.

Un exemple:

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

Après demande:

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