Gerenciando várias conexões NSURLConnection assíncronas

88

Eu tenho uma tonelada de códigos repetidos em minha classe que se parecem com o seguinte:

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

O problema com as solicitações assíncronas é quando você tem várias solicitações saindo e tem um delegado designado para tratá-las todas como uma entidade, muitas ramificações e códigos feios começam a ser formulados:

Que tipo de dados estamos recebendo? Se contiver isso, faça aquilo, senão faça outro. Acho que seria útil ser capaz de marcar essas solicitações assíncronas, como se você fosse capaz de marcar visualizações com IDs.

Eu estava curioso para saber qual estratégia é mais eficiente para gerenciar uma classe que lida com várias solicitações assíncronas.

Coocoo4Cocoa
fonte

Respostas:

77

Eu rastreio as respostas em um CFMutableDictionaryRef digitado pelo NSURLConnection associado a ele. ie:

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

Pode parecer estranho usar isso em vez de NSMutableDictionary, mas eu faço isso porque este CFDictionary apenas retém suas chaves (o NSURLConnection), enquanto o NSDictionary copia suas chaves (e NSURLConnection não suporta a cópia).

Feito isso:

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

e agora eu tenho um dicionário "info" de dados para cada conexão que posso usar para rastrear informações sobre a conexão e o dicionário "info" já contém um objeto de dados mutável que posso usar para armazenar os dados de resposta conforme eles chegam.

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    NSMutableDictionary *connectionInfo =
        CFDictionaryGetValue(connectionToInfoMapping, connection);
    [[connectionInfo objectForKey:@"receivedData"] appendData:data];
}
Matt Gallagher
fonte
Uma vez que é possível que duas ou mais conexões assíncronas possam entrar nos métodos delegados ao mesmo tempo, há algo específico que alguém precise fazer para garantir o comportamento correto?
PlagueHammer
(Eu criei uma nova pergunta aqui perguntando isto: stackoverflow.com/questions/1192294/… )
PlagueHammer
3
Isso não é seguro para thread se o delegado estiver sendo chamado de vários threads. Você deve usar bloqueios de exclusão mútua para proteger as estruturas de dados. Uma solução melhor é criar uma subclasse de NSURLConnection e adicionar respostas e referências de dados como variáveis ​​de instância. Estou fornecendo uma resposta mais detalhada explicando isso na pergunta do Nocturne: stackoverflow.com/questions/1192294/…
James Wald
4
Aldi ... é seguro para thread, desde que você inicie todas as conexões do mesmo thread (o que você pode fazer facilmente invocando seu método de conexão inicial usando performSelector: onThread: withObject: waitUntilDone :). Colocar todas as conexões em um NSOperationQueue tem problemas diferentes se você tentar iniciar mais conexões do que o máximo de operações simultâneas da fila (as operações ficam enfileiradas em vez de serem executadas simultaneamente). NSOperationQueue funciona bem para operações vinculadas à CPU, mas para operações vinculadas à rede, é melhor usar uma abordagem que não use um pool de threads de tamanho fixo.
Matt Gallagher
1
Só queria compartilhar isso para iOS 6.0 e superior, você pode usar um em [NSMapTable weakToStrongObjectsMapTable]vez de um CFMutableDictionaryRefe evitar o aborrecimento. Funcionou bem para mim.
Shay Aviv
19

Tenho um projeto em que tenho duas NSURLConnections distintas e queria usar o mesmo delegado. O que fiz foi criar duas propriedades em minha classe, uma para cada conexão. Então, no método delegado, eu verifico se qual conexão é


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

Isso também me permite cancelar uma conexão específica por nome quando necessário.

Jbarnhart
fonte
tenha cuidado, isso é problemático, pois haverá condições de corrida
adito
Como você atribui os nomes (savingConnection e sharingReturnedData) para cada conexão em primeiro lugar?
jsherk
@adit, não, não há condição de corrida inerente a este código. Você teria que ir muito longe com o código de criação de conexão para criar uma condição de corrida
Mike Abdullah
sua 'solução' é exatamente o que a pergunta original procura evitar, citando acima: '... muitos códigos ramificados e feios começam a se formular ...'
stefanB
1
@adit Por que isso levará a uma condição de corrida? É um novo conceito para mim.
guptron
16

A subclasse de NSURLConnection para conter os dados é limpa, menos código do que algumas das outras respostas, é mais flexível e requer menos consideração sobre o gerenciamento de referência.

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

Use-o como faria com NSURLConnection e acumule os dados em sua propriedade de dados:

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

É isso aí.

Se quiser ir mais longe, você pode adicionar um bloco para servir como um retorno de chamada com apenas mais algumas linhas de código:

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

Defina assim:

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

e invoque-o quando o carregamento terminar desta forma:

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

Você pode estender o bloco para aceitar parâmetros ou apenas passar o DataURLConnection como um argumento para o método que precisa dele dentro do bloco no-args como mostrado

Pat Niemeyer
fonte
Esta é uma resposta fantástica que funcionou muito bem para o meu caso. Muito simples e limpo!
jwarrent
8

ESTA NÃO É UMA NOVA RESPOSTA. POR FAVOR, DEIXE-ME MOSTRAR COMO EU FIZ

Para distinguir NSURLConnection diferentes dentro dos métodos delegados da mesma classe, eu uso NSMutableDictionary, para definir e remover o NSURLConnection, usando seu (NSString *)description chave como.

O objeto que escolhi setObject:forKeyé o URL exclusivo que é usado para iniciar NSURLRequest, os NSURLConnectionusos.

Depois de definir NSURLConnection é avaliada em

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

Uma abordagem que tomei é não usar o mesmo objeto como delegado para cada conexão. Em vez disso, crio uma nova instância de minha classe de análise para cada conexão que é disparada e defino o delegado para essa instância.

Brad, o cara dos aplicativos
fonte
Encapsulamento muito melhor com relação a uma conexão.
Kedar Paranjape
4

Experimente minha classe personalizada, MultipleDownload , que trata de tudo isso para você.

leonho
fonte
no iOS6 não pode usar o NSURLConnection como a chave.
user501836
2

Normalmente crio uma série de dicionários. Cada dicionário tem algumas informações de identificação, um objeto NSMutableData para armazenar a resposta e a própria conexão. Quando um método de delegado de conexão é acionado, procuro no dicionário da conexão e o manejo de acordo.

Ben Gottlieb
fonte
Ben, estaria tudo bem em pedir-lhe um pedaço de código de amostra? Estou tentando imaginar como você está fazendo isso, mas não está tudo aí.
Coocoo4Cocoa 01 de
Em particular Ben, como você consulta o dicionário? Você não pode ter um dicionário de dicionários, pois NSURLConnection não implementa NSCopying (portanto, não pode ser usado como uma chave).
Adam Ernst
Matt tem uma excelente solução abaixo usando CFMutableDictionary, mas eu uso uma variedade de dicionários. Uma pesquisa requer uma iteração. Não é o mais eficiente, mas é rápido o suficiente.
Ben Gottlieb
2

Uma opção é simplesmente criar uma subclasse NSURLConnection e adicionar uma -tag ou método semelhante. O design do NSURLConnection é intencionalmente muito básico, então isso é perfeitamente aceitável.

Ou talvez você possa criar uma classe MyURLConnectionController que seja responsável por criar e coletar os dados de uma conexão. Então, ele só teria que informar seu objeto controlador principal quando o carregamento for concluído.

Mike Abdullah
fonte
2

no iOS5 e acima, você pode apenas usar o método de classe sendAsynchronousRequest:queue:completionHandler:

Não há necessidade de monitorar as conexões, pois a resposta retorna no manipulador de conclusão.

Yariv Nissim
fonte
1

Eu gosto de ASIHTTPRequest .

ruipacheco
fonte
Eu realmente gosto da implementação de 'blocos' em ASIHTTPRequest - é como Tipos internos anônimos em Java. Isso supera todas as outras soluções em termos de limpeza e organização do código.
Matt Lyons
1

Conforme apontado por outras respostas, você deve armazenar connectionInfo em algum lugar e procurá-los por conexão.

O tipo de dados mais natural para isso é NSMutableDictionary, mas não pode aceitarNSURLConnection como chaves, pois as conexões não podem ser copiadas.

Outra opção para usar NSURLConnectionscomo chaves NSMutableDictionaryé usar NSValue valueWithNonretainedObject]:

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

Decidi criar uma subclasse NSURLConnection e adicionar uma tag, delegado e um NSMutabaleData. Eu tenho uma classe DataController que lida com todo o gerenciamento de dados, incluindo as solicitações. Eu criei um protocolo DataControllerDelegate, para que visões / objetos individuais possam ouvir o DataController para descobrir quando suas solicitações foram concluídas e, se necessário, quanto foi baixado ou erros. A classe DataController pode usar a subclasse NSURLConnection para iniciar uma nova solicitação e salvar o delegado que deseja ouvir o DataController para saber quando a solicitação foi concluída. Esta é minha solução de trabalho no XCode 4.5.2 e ios 6.

O arquivo DataController.h que declara o protocolo DataControllerDelegate). O DataController também é um 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

Os principais métodos no arquivo 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 para iniciar um pedido: [[NSURLConnectionWithDelegate alloc] initWithRequest:request delegate:self startImmediately:YES tag:@"Login" dataDelegate:delegate];

O 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 o 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
fonte
0

Cada NSURLConnection tem um atributo hash, você pode discriminar todos por este atributo.

Por exemplo, eu preciso manter certas informações antes e depois da conexão, então meu RequestManager tem um NSMutableDictionary para fazer isso.

Um exemplo:

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

Após solicitação:

- (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
fonte