Verwalten mehrerer asynchroner NSURLConnection-Verbindungen

88

Ich habe eine Menge sich wiederholenden Codes in meiner Klasse, der wie folgt aussieht:

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

Das Problem bei asynchronen Anforderungen besteht darin, dass bei verschiedenen Anfragen und einem Delegaten, der sie alle als eine Entität behandelt, eine Menge verzweigter und hässlicher Codes formuliert werden:

Welche Art von Daten erhalten wir zurück? Wenn es dies enthält, machen Sie das, sonst machen Sie andere. Ich denke, es wäre nützlich, diese asynchronen Anforderungen mit Tags versehen zu können, ähnlich wie Sie Ansichten mit IDs versehen können.

Ich war gespannt, welche Strategie für die Verwaltung einer Klasse, die mehrere asynchrone Anforderungen verarbeitet, am effizientesten ist.

Coocoo4Cocoa
quelle

Antworten:

77

Ich verfolge Antworten in einem CFMutableDictionaryRef, das von der damit verbundenen NSURLConnection eingegeben wird. dh:

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

Es mag seltsam erscheinen, dies anstelle von NSMutableDictionary zu verwenden, aber ich mache es, weil dieses CFDictionary nur seine Schlüssel (die NSURLConnection) behält, während NSDictionary seine Schlüssel kopiert (und NSURLConnection das Kopieren nicht unterstützt).

Sobald das erledigt ist:

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

und jetzt habe ich ein "Info" -Datenwörterbuch für jede Verbindung, mit dem ich Informationen über die Verbindung verfolgen kann, und das "Info" -Wörterbuch enthält bereits ein veränderbares Datenobjekt, mit dem ich die eingehenden Antwortdaten speichern kann.

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    NSMutableDictionary *connectionInfo =
        CFDictionaryGetValue(connectionToInfoMapping, connection);
    [[connectionInfo objectForKey:@"receivedData"] appendData:data];
}
Matt Gallagher
quelle
Gibt es etwas Spezifisches, das getan werden muss, um ein korrektes Verhalten sicherzustellen, da möglicherweise zwei oder mehr asynchrone Verbindungen gleichzeitig in die Delegatmethoden eingegeben werden?
PlagueHammer
(Ich habe hier eine neue Frage erstellt: stackoverflow.com/questions/1192294/… )
PlagueHammer
3
Dies ist nicht threadsicher, wenn der Delegat von mehreren Threads aufgerufen wird. Sie müssen gegenseitige Ausschlusssperren verwenden, um die Datenstrukturen zu schützen. Eine bessere Lösung besteht darin, NSURLConnection in Unterklassen zu unterteilen und Antwort- und Datenreferenzen als Instanzvariablen hinzuzufügen. Ich gebe
James Wald
4
Aldi ... es ist threadsicher, vorausgesetzt, Sie starten alle Verbindungen von demselben Thread (was Sie einfach tun können, indem Sie Ihre Startverbindungsmethode mit performSelector: onThread: withObject: waitUntilDone :) aufrufen :). Das Einfügen aller Verbindungen in eine NSOperationQueue hat unterschiedliche Probleme, wenn Sie versuchen, mehr Verbindungen als die maximal gleichzeitigen Vorgänge der Warteschlange zu starten (Vorgänge werden in die Warteschlange gestellt, anstatt gleichzeitig ausgeführt zu werden). NSOperationQueue funktioniert gut für CPU-gebundene Operationen, aber für netzwerkgebundene Operationen ist es besser, einen Ansatz zu verwenden, der keinen Thread-Pool mit fester Größe verwendet.
Matt Gallagher
1
Ich wollte nur mitteilen, dass Sie für iOS 6.0 und höher a [NSMapTable weakToStrongObjectsMapTable]anstelle von a verwenden CFMutableDictionaryRefund den Aufwand sparen können. Hat gut für mich funktioniert.
Shay Aviv
19

Ich habe ein Projekt, in dem ich zwei verschiedene NSURLConnections habe und denselben Delegaten verwenden wollte. Ich habe zwei Eigenschaften in meiner Klasse erstellt, eine für jede Verbindung. Dann überprüfe ich in der Delegate-Methode, ob es sich um eine Verbindung handelt


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

Auf diese Weise kann ich bei Bedarf auch eine bestimmte Verbindung nach Namen abbrechen.

jbarnhart
quelle
seien Sie vorsichtig , dies problematisch ist , da es Rennbedingungen haben
Stollens
Wie weisen Sie die Namen (saveConnection und sharedReturnedData) für jede Verbindung überhaupt zu?
Jsherk
@adit, nein, diesem Code ist keine Racebedingung inhärent. Sie müssten mit dem Verbindungserstellungscode ziemlich weit aus dem Weg gehen, um eine Rennbedingung zu erstellen
Mike Abdullah
Ihre 'Lösung' ist genau das, was die ursprüngliche Frage zu vermeiden versucht, indem Sie von oben zitieren: '... viel verzweigter und hässlicher Code beginnt zu formulieren ...'
stefanB
1
@adit Warum führt dies zu einer Rennbedingung? Es ist ein neues Konzept für mich.
Guptron
16

Die Unterklasse von NSURLConnection zum Speichern der Daten ist sauber, weniger Code als einige der anderen Antworten, flexibler und erfordert weniger Überlegungen zum Referenzmanagement.

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

Verwenden Sie es wie NSURLConnection und sammeln Sie die Daten in seiner Dateneigenschaft:

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

Das ist es.

Wenn Sie weiter gehen möchten, können Sie einen Block hinzufügen, der als Rückruf mit nur ein paar weiteren Codezeilen dient:

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

Stellen Sie es so ein:

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

und rufen Sie es auf, wenn das Laden wie folgt beendet ist:

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

Sie können den Block erweitern, um Parameter zu akzeptieren, oder einfach die DataURLConnection als Argument an die Methode übergeben, die sie wie gezeigt im Block no-args benötigt

Pat Niemeyer
quelle
Dies ist eine fantastische Antwort, die für meinen Fall sehr gut funktioniert hat. Sehr einfach und sauber!
Jwarrent
8

Dies ist keine neue Antwort. Bitte lassen Sie mich Ihnen zeigen, wie ich es getan habe

Um verschiedene NSURLConnection innerhalb der Delegatmethoden derselben Klasse zu unterscheiden, verwende ich NSMutableDictionary, um die NSURLConnection mithilfe ihres (NSString *)descriptionSchlüssels festzulegen und zu entfernen .

Das Objekt, für setObject:forKeydas ich mich entschieden habe, ist die eindeutige URL, die zum Initiieren NSURLRequestder NSURLConnectionVerwendungen verwendet wird.

Nach dem Einstellen wird NSURLConnection bei ausgewertet

-(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
quelle
5

Ein Ansatz, den ich gewählt habe, besteht darin, nicht für jede Verbindung dasselbe Objekt wie der Delegat zu verwenden. Stattdessen erstelle ich für jede Verbindung, die ausgelöst wird, eine neue Instanz meiner Parsing-Klasse und setze den Delegaten auf diese Instanz.

Brad Der App-Typ
quelle
Viel bessere Kapselung in Bezug auf eine Verbindung.
Kedar Paranjape
4

Probieren Sie meine benutzerdefinierte Klasse MultipleDownload aus , die all dies für Sie erledigt.

leonho
quelle
Unter iOS6 kann die NSURLConnection nicht als Schlüssel verwendet werden.
user501836
2

Normalerweise erstelle ich eine Reihe von Wörterbüchern. Jedes Wörterbuch enthält einige identifizierende Informationen, ein NSMutableData-Objekt zum Speichern der Antwort und die Verbindung selbst. Wenn eine Verbindungsdelegatmethode ausgelöst wird, schaue ich im Wörterbuch der Verbindung nach und behandle es entsprechend.

Ben Gottlieb
quelle
Ben, wäre es in Ordnung, dich nach einem Beispielcode zu fragen? Ich versuche mir vorzustellen, wie du es machst, aber es ist nicht alles da.
Coocoo4Cocoa
Insbesondere Ben, wie siehst du das Wörterbuch nach? Sie können kein Wörterbuch mit Wörterbüchern haben, da NSURLConnection NSCopying nicht implementiert (daher kann es nicht als Schlüssel verwendet werden).
Adam Ernst
Matt hat unten eine ausgezeichnete Lösung mit CFMutableDictionary, aber ich verwende eine Reihe von Wörterbüchern. Eine Suche erfordert eine Iteration. Es ist nicht das effizienteste, aber es ist schnell genug.
Ben Gottlieb
2

Eine Möglichkeit besteht darin, NSURLConnection selbst zu unterklassifizieren und ein -tag oder eine ähnliche Methode hinzuzufügen. Das Design von NSURLConnection ist absichtlich sehr nackt, daher ist dies durchaus akzeptabel.

Oder Sie können eine MyURLConnectionController-Klasse erstellen, die für das Erstellen und Sammeln der Daten einer Verbindung verantwortlich ist. Es müsste dann erst Ihr Hauptcontrollerobjekt informieren, wenn das Laden abgeschlossen ist.

Mike Abdullah
quelle
2

In iOS5 und höher können Sie einfach die Klassenmethode verwenden sendAsynchronousRequest:queue:completionHandler:

Verbindungen müssen nicht nachverfolgt werden, da die Antwort im Completion-Handler zurückgegeben wird.

Yariv Nissim
quelle
1

Ich mag ASIHTTPRequest .

Ruipacheco
quelle
Ich mag die Implementierung von 'Blöcken' in ASIHTTPRequest sehr - es ist genau wie bei anonymen inneren Typen in Java. Dies übertrifft alle anderen Lösungen in Bezug auf Code-Sauberkeit und Organisation.
Matt Lyons
1

Wie in anderen Antworten erwähnt, sollten Sie connectionInfo irgendwo speichern und nach Verbindung suchen.

Der natürlichste Datentyp hierfür ist NSMutableDictionary, er kann jedoch nicht NSURLConnectionals Schlüssel akzeptiert werden, da Verbindungen nicht kopierbar sind.

Eine weitere Option für die Verwendung NSURLConnectionsals Schlüssel NSMutableDictionaryist die Verwendung von NSValue valueWithNonretainedObject]:

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

Ich habe beschlossen, NSURLConnection in eine Unterklasse zu unterteilen und ein Tag, einen Delegaten und eine NSMutabaleData hinzuzufügen. Ich habe eine DataController-Klasse, die die gesamte Datenverwaltung einschließlich der Anforderungen übernimmt. Ich habe ein DataControllerDelegate-Protokoll erstellt, damit einzelne Ansichten / Objekte den DataController abhören können, um herauszufinden, wann ihre Anforderungen abgeschlossen wurden und bei Bedarf, wie viel heruntergeladen wurde oder welche Fehler aufgetreten sind. Die DataController-Klasse kann die NSURLConnection-Unterklasse verwenden, um eine neue Anforderung zu starten und den Delegaten zu speichern, der den DataController abhören möchte, um zu erfahren, wann die Anforderung abgeschlossen ist. Dies ist meine Arbeitslösung in XCode 4.5.2 und iOS 6.

Die Datei DataController.h, die das DataControllerDelegate-Protokoll deklariert. Der DataController ist auch ein 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

Die wichtigsten Methoden in der Datei 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]];
}

Und um eine Anfrage zu starten: [[NSURLConnectionWithDelegate alloc] initWithRequest:request delegate:self startImmediately:YES tag:@"Login" dataDelegate:delegate];

Die 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

Und die 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
quelle
0

Jede NSURLConnection hat ein Hash-Attribut. Sie können alle anhand dieses Attributs unterscheiden.

Zum Beispiel muss ich bestimmte Informationen vor und nach der Verbindung pflegen, damit mein RequestManager ein NSMutableDictionary hat, um dies zu tun.

Ein Beispiel:

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

Nach Anfrage:

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