Zarządzanie wieloma asynchronicznymi połączeniami NSURLConnection

88

W mojej klasie mam mnóstwo powtarzającego się kodu, który wygląda następująco:

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

Problem z żądaniami asynchronicznymi polega na tym, że gdy masz różne żądania i masz przypisanego delegata, który ma traktować je wszystkie jako jedną całość, wiele rozgałęzionych i brzydkich kodów zaczyna formułować:

Jakie dane odzyskujemy? Jeśli to zawiera, zrób to, w przeciwnym razie zrób inne. Myślę, że przydałaby się możliwość tagowania tych asynchronicznych żądań, tak jakbyś mógł oznaczać widoki identyfikatorami.

Byłem ciekawy, jaka strategia jest najbardziej efektywna w zarządzaniu klasą, która obsługuje wiele asynchronicznych żądań.

Coocoo4Cocoa
źródło

Odpowiedzi:

77

Śledzę odpowiedzi w CFMutableDictionaryRef z kluczem związanym z NSURLConnection. to znaczy:

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

Używanie tego zamiast NSMutableDictionary może wydawać się dziwne, ale robię to, ponieważ ten CFDictionary zachowuje tylko swoje klucze (NSURLConnection), podczas gdy NSDictionary kopiuje swoje klucze (a NSURLConnection nie obsługuje kopiowania).

Gdy to zrobisz:

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

i teraz mam słownik „info” danych dla każdego połączenia, którego mogę użyć do śledzenia informacji o połączeniu, a słownik „info” zawiera już zmienny obiekt danych, którego mogę używać do przechowywania danych odpowiedzi, gdy tylko nadejdą.

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    NSMutableDictionary *connectionInfo =
        CFDictionaryGetValue(connectionToInfoMapping, connection);
    [[connectionInfo objectForKey:@"receivedData"] appendData:data];
}
Matt Gallagher
źródło
Ponieważ jest możliwe, że dwa lub więcej połączeń asynchronicznych może wejść do metod delegata naraz, czy jest coś konkretnego, co należałoby zrobić, aby zapewnić prawidłowe zachowanie?
PlagueHammer
(
Utworzyłem
3
Nie jest to bezpieczne wątkowo, jeśli delegat jest wywoływany z wielu wątków. Aby chronić struktury danych, należy używać blokad wzajemnego wykluczania. Lepszym rozwiązaniem jest utworzenie podklasy NSURLConnection i dodanie odpowiedzi i odwołań do danych jako zmiennych instancji. Bardziej szczegółowej odpowiedzi udzielam na pytanie Nocturne'a: stackoverflow.com/questions/1192294/…
James Wald
4
Aldi ... jest bezpieczny wątkowo pod warunkiem, że uruchamiasz wszystkie połączenia z tego samego wątku (co możesz łatwo zrobić, wywołując metodę uruchamiania połączenia za pomocą performSelector: onThread: withObject: waitUntilDone :). Umieszczenie wszystkich połączeń w NSOperationQueue powoduje inne problemy, jeśli spróbujesz uruchomić więcej połączeń niż maksymalna liczba jednoczesnych operacji kolejki (operacje są umieszczane w kolejce zamiast działać jednocześnie). NSOperationQueue działa dobrze w przypadku operacji związanych z procesorem, ale w przypadku operacji związanych z siecią lepiej jest użyć podejścia, które nie używa puli wątków o stałym rozmiarze.
Matt Gallagher
1
Chciałem tylko udostępnić to dla iOS 6.0 i nowszych wersji, możesz użyć [NSMapTable weakToStrongObjectsMapTable]zamiast CFMutableDictionaryRefai zaoszczędzić kłopotów. Pracował dobrze dla mnie.
Shay Aviv
19

Mam projekt, w którym mam dwa różne NSURLConnections i chciałem użyć tego samego delegata. Stworzyłem dwie właściwości w mojej klasie, po jednej dla każdego połączenia. Następnie w metodzie delegata sprawdzam, czy to połączenie


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

Pozwala mi to również w razie potrzeby anulować określone połączenie według nazwy.

jbarnhart
źródło
uważaj, jest to problematyczne, ponieważ będą miały warunki wyścigu
sztolnia
Jak w pierwszej kolejności przypisujesz nazwy (saveConnection i sharingReturnedData) dla każdego połączenia?
jsherk
@adit, nie, nie ma warunku wyścigu związanego z tym kodem. Będziesz musiał zejść z drogi z kodem tworzenia połączenia, aby stworzyć warunek wyścigu
Mike Abdullah
Twoje
``
1
@adit Dlaczego doprowadzi to do stanu wyścigu? To dla mnie nowa koncepcja.
guptron,
16

Podklasa NSURLConnection do przechowywania danych jest czysta, zawiera mniej kodu niż niektóre inne odpowiedzi, jest bardziej elastyczna i wymaga mniej uwagi na temat zarządzania referencjami.

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

Używaj go tak, jak NSURLConnection i zbieraj dane w jego właściwości 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];
}

Otóż ​​to.

Jeśli chcesz pójść dalej, możesz dodać blok, który będzie służył jako wywołanie zwrotne za pomocą kilku dodatkowych linii kodu:

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

Ustaw to tak:

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

i wywołaj go po zakończeniu ładowania w następujący sposób:

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

Możesz rozszerzyć blok, aby akceptował parametry lub po prostu przekazać DataURLConnection jako argument do metody, która go potrzebuje w bloku no-args, jak pokazano

Pat Niemeyer
źródło
To fantastyczna odpowiedź, która bardzo dobrze się sprawdziła w moim przypadku. Bardzo proste i czyste!
jwarrent
8

TO NIE JEST NOWA ODPOWIEDŹ. POKAŻ, JAK CIĘ ZROBIŁEM

Aby rozróżnić różne NSURLConnection w ramach metod delegatów tej samej klasy, używam NSMutableDictionary, aby ustawić i usunąć NSURLConnection, używając jego (NSString *)descriptionjako klucza.

Obiekt, który wybrałem, setObject:forKeyto unikalny adres URL używany do inicjowania NSURLRequestrozszerzeniaNSURLConnection zastosowań.

Po ustawieniu NSURLConnection jest oceniane pod adresem

-(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
źródło
5

Jedną z metod, które zastosowałem, jest nie używanie tego samego obiektu jako delegata dla każdego połączenia. Zamiast tego tworzę nowe wystąpienie mojej klasy analizy dla każdego połączenia, które jest wyłączane i ustawiam delegata na to wystąpienie.

Brad The App Guy
źródło
Znacznie lepsza hermetyzacja w odniesieniu do jednego połączenia.
Kedar Paranjape
4

Wypróbuj moją niestandardową klasę MultipleDownload , która obsługuje wszystkie te elementy.

leonho
źródło
w systemie iOS6 nie można użyć NSURLConnection jako klucza.
user501836
2

Zwykle tworzę szereg słowników. Każdy słownik zawiera trochę informacji identyfikujących, obiekt NSMutableData do przechowywania odpowiedzi oraz samo połączenie. Po uruchomieniu metody delegata połączenia wyszukuję słownik połączenia i odpowiednio go obsługuję.

Ben Gottlieb
źródło
Ben, czy możesz poprosić cię o przykładowy kod? Próbuję sobie wyobrazić, jak to robisz, ale to nie wszystko.
Coocoo4Cocoa
W szczególności Ben, jak przeglądasz słownik? Nie możesz mieć słownika słowników, ponieważ NSURLConnection nie implementuje NSCopying (więc nie może być używany jako klucz).
Adam Ernst,
Matt ma poniżej doskonałe rozwiązanie przy użyciu CFMutableDictionary, ale ja używam szeregu słowników. Wyszukiwanie wymaga iteracji. To nie jest najbardziej wydajne, ale jest wystarczająco szybkie.
Ben Gottlieb,
2

Jedną z opcji jest samodzielne utworzenie podklasy NSURLConnection i dodanie -tag lub podobnej metody. Konstrukcja NSURLConnection to celowo bardzo gołe kości, więc jest to całkowicie akceptowalne.

A może możesz utworzyć klasę MyURLConnectionController, która jest odpowiedzialna za tworzenie i zbieranie danych połączenia. Musiałby wtedy tylko poinformować twój główny obiekt kontrolera po zakończeniu ładowania.

Mike Abdullah
źródło
2

w iOS5 i nowszych możesz po prostu użyć metody class sendAsynchronousRequest:queue:completionHandler:

Nie ma potrzeby śledzenia połączeń, ponieważ odpowiedź jest zwracana w module obsługi zakończenia.

Yariv Nissim
źródło
1

Lubię ASIHTTPRequest .

ruipacheco
źródło
Bardzo podoba mi się implementacja „bloków” w ASIHTTPRequest - to zupełnie jak anonimowe typy wewnętrzne w Javie. To przebija wszystkie inne rozwiązania pod względem czystości i organizacji kodu.
Matt Lyons,
1

Jak wskazały inne odpowiedzi, powinieneś gdzieś przechowywać connectionInfo i wyszukiwać je po połączeniu.

Najbardziej naturalnym typem danych jest to NSMutableDictionary, ale nie można go zaakceptowaćNSURLConnection jako kluczy, ponieważ połączeń nie można skopiować.

Inną opcją użycia NSURLConnectionsjako kluczy w NSMutableDictionaryjest użycie NSValue valueWithNonretainedObject]:

NSMutableDictionary* dict = [NSMutableDictionary dictionary];
NSValue *key = [NSValue valueWithNonretainedObject:aConnection]
/* store: */
[dict setObject:connInfo forKey:key];
/* lookup: */
[dict objectForKey:key];
mfazekas
źródło
0

Postanowiłem podklasę NSURLConnection i dodać tag, delegata i NSMutabaleData. Mam klasę DataController, która obsługuje całe zarządzanie danymi, w tym żądania. Stworzyłem protokół DataControllerDelegate, aby poszczególne widoki / obiekty mogły nasłuchiwać DataController, aby dowiedzieć się, kiedy ich żądania zostały zakończone, a jeśli to konieczne, ile zostało pobranych lub błędy. Klasa DataController może użyć podklasy NSURLConnection do uruchomienia nowego żądania i zapisać delegata, który chce nasłuchiwać DataController, aby wiedzieć, kiedy żądanie zakończyło się. To jest moje działające rozwiązanie w XCode 4.5.2 i iOS 6.

Plik DataController.h, który deklaruje protokół DataControllerDelegate). DataController jest również singletonem:

@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

Kluczowe metody w pliku 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]];
}

Aby złożyć wniosek: [[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

Oraz 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
źródło
0

Każdy NSURLConnection ma atrybut skrótu, możesz rozróżniać wszystko według tego atrybutu.

Na przykład muszę zachować pewne informacje przed i po połączeniu, więc mój RequestManager ma NSMutableDictionary, aby to zrobić.

Przykład:

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

Na życzenie:

- (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
źródło