Управление несколькими асинхронными соединениями NSURLConnection

88

У меня в классе много повторяющегося кода, который выглядит следующим образом:

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

Проблема с асинхронными запросами заключается в том, что у вас выполняются различные запросы, и у вас есть делегат, которому назначено обрабатывать их все как одну сущность, много ветвлений и уродливый код начинает формулировать:

Какие данные мы возвращаем? Если он содержит это, сделайте это, иначе сделайте другое. Я думаю, было бы полезно иметь возможность помечать эти асинхронные запросы, как если бы вы могли помечать представления идентификаторами.

Мне было любопытно, какая стратегия наиболее эффективна для управления классом, который обрабатывает несколько асинхронных запросов.

Coocoo4Cocoa
источник

Ответы:

77

Я отслеживаю ответы в CFMutableDictionaryRef с ключом связанного с ним NSURLConnection. то есть:

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

Может показаться странным использовать это вместо NSMutableDictionary, но я делаю это, потому что этот CFDictionary сохраняет только свои ключи (NSURLConnection), тогда как NSDictionary копирует свои ключи (а NSURLConnection не поддерживает копирование).

Как только это будет сделано:

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

и теперь у меня есть "информационный" словарь данных для каждого соединения, который я могу использовать для отслеживания информации о соединении, а "информационный" словарь уже содержит изменяемый объект данных, который я могу использовать для хранения данных ответа по мере их поступления.

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    NSMutableDictionary *connectionInfo =
        CFDictionaryGetValue(connectionToInfoMapping, connection);
    [[connectionInfo objectForKey:@"receivedData"] appendData:data];
}
Мэтт Галлахер
источник
Поскольку возможно, что два или более асинхронных соединения могут входить в методы делегата одновременно, есть ли что-то конкретное, что нужно сделать для обеспечения правильного поведения?
PlagueHammer
задаю
3
Это не является потокобезопасным, если делегат вызывается из нескольких потоков. Вы должны использовать блокировки взаимного исключения для защиты структур данных. Лучшее решение - создать подкласс NSURLConnection и добавить ответы и ссылки на данные в качестве переменных экземпляра. Я даю более подробный ответ, объясняющий это, на вопрос Ноктюрна: stackoverflow.com/questions/1192294/…
Джеймс Уолд
4
Aldi ... это İŞ поточно при условии , вы начинаете все соединения из одной и той же нити (которые вы можете сделать легко, вызывая свой метод подключения запуска с помощью performSelector: onThread: withObject: waitUntilDone :). Помещение всех подключений в NSOperationQueue имеет разные проблемы, если вы пытаетесь запустить больше подключений, чем максимальное количество одновременных операций очереди (операции ставятся в очередь, а не выполняются одновременно). NSOperationQueue хорошо работает для операций, связанных с ЦП, но для операций с привязкой к сети вам лучше использовать подход, который не использует пул потоков фиксированного размера.
Мэтт Галлахер
1
Просто хотел поделиться тем, что для iOS 6.0 и более поздних версий вы можете использовать [NSMapTable weakToStrongObjectsMapTable]вместо a CFMutableDictionaryRefи сэкономить хлопоты. Хорошо сработало для меня.
Шай Авив
19

У меня есть проект, в котором у меня есть два разных NSURLConnection, и я хотел использовать один и тот же делегат. Я создал в своем классе два свойства, по одному для каждого соединения. Затем в методе делегата я проверяю, какое это соединение


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

Это также позволяет мне при необходимости отменить конкретное соединение по имени.

Jbarnhart
источник
будьте осторожны, это проблематично, так как это будет иметь условия гонки
штольн
Как вы в первую очередь назначаете имена (saveConnection и sharingReturnedData) для каждого соединения?
jsherk 06
@adit, нет, для этого кода нет состояния гонки. Чтобы создать состояние гонки, вам придется пойти довольно далеко с кодом создания соединения
Майк Абдулла,
ваше «решение» - это именно то, чего
пытается
1
@adit Почему это приведет к состоянию гонки? Для меня это новая концепция.
guptron 05
16

Создание подкласса NSURLConnection для хранения данных является чистым, имеет меньше кода, чем некоторые другие ответы, является более гибким и требует меньше размышлений об управлении ссылками.

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

Используйте его так же, как NSURLConnection, и накапливайте данные в его свойстве 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];
}

Вот и все.

Если вы хотите пойти дальше, вы можете добавить блок, который будет выполнять функцию обратного вызова, с помощью еще пары строк кода:

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

Установите это так:

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

и вызвать его, когда загрузка завершится следующим образом:

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

Вы можете расширить блок для приема параметров или просто передать DataURLConnection в качестве аргумента методу, который в нем нуждается, в блоке no-args, как показано

Пэт Нимейер
источник
Это фантастический ответ, который действительно хорошо сработал в моем случае. Очень просто и чисто!
jwarrent
8

ЭТО НЕ НОВЫЙ ОТВЕТ. ПОЖАЛУЙСТА, ДАЙТЕ МНЕ ПОКАЗАТЬ ВАМ, КАК Я ДЕЛАЛ

Чтобы различать разные NSURLConnection в методах делегата одного и того же класса, я использую NSMutableDictionary, чтобы установить и удалить NSURLConnection, используя его в (NSString *)descriptionкачестве ключа.

Объект, который я выбрал, setObject:forKey- это уникальный URL-адрес, который используется для запуска NSURLRequest, NSURLConnectionuses.

После установки NSURLConnection оценивается на

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

Один из моих подходов - не использовать один и тот же объект в качестве делегата для каждого соединения. Вместо этого я создаю новый экземпляр моего класса синтаксического анализа для каждого отключенного соединения и устанавливаю делегат для этого экземпляра.

Брэд, парень из приложения
источник
Намного лучшая инкапсуляция по отношению к одному соединению.
Кедар Паранджапе
4

Попробуйте мой собственный класс MultipleDownload , который все это сделает за вас.

Леонхо
источник
на iOS6 нельзя использовать NSURLConnection в качестве ключа.
user501836
2

Обычно я создаю массив словарей. Каждый словарь имеет бит идентифицирующей информации, объект NSMutableData для хранения ответа и само соединение. Когда срабатывает метод делегата соединения, я ищу словарь соединения и обрабатываю его соответствующим образом.

Бен Готтлиб
источник
Бен, можно ли попросить у вас образец кода? Я пытаюсь представить, как вы это делаете, но это еще не все.
Coocoo4Cocoa 01
В частности, Бен, как найти словарь? У вас не может быть словаря словарей, поскольку NSURLConnection не реализует NSCopying (поэтому его нельзя использовать в качестве ключа).
Адам Эрнст,
У Мэтта есть отличное решение с использованием CFMutableDictionary ниже, но я использую массив словарей. Для поиска требуется итерация. Не самый эффективный, но достаточно быстрый.
Бен Готтлиб
2

Один из вариантов - просто создать подкласс NSURLConnection самостоятельно и добавить -tag или аналогичный метод. Дизайн NSURLConnection намеренно очень простой, поэтому это вполне приемлемо.

Или, возможно, вы могли бы создать класс MyURLConnectionController, который отвечает за создание и сбор данных подключения. Тогда ему нужно будет только сообщить вашему основному объекту контроллера после завершения загрузки.

Майк Абдулла
источник
2

в iOS5 и выше вы можете просто использовать метод класса sendAsynchronousRequest:queue:completionHandler:

Нет необходимости отслеживать соединения, поскольку ответ возвращается в обработчике завершения.

Ярив Нисим
источник
1

Мне нравится ASIHTTPRequest .

Ruipacheco
источник
Мне очень нравится реализация «блоков» в ASIHTTPRequest - это как анонимные внутренние типы в Java. Это превосходит все другие решения с точки зрения чистоты и организации кода.
Мэтт Лайонс
1

Как указано в других ответах, вы должны где-то хранить connectionInfo и искать их по подключению.

Наиболее естественный тип данных для этого NSMutableDictionary, но он не может приниматьNSURLConnection качестве ключей, поскольку соединения не копируются.

Другой вариант для использования в NSURLConnectionsкачестве ключей в NSMutableDictionaryиспользует NSValue valueWithNonretainedObject]:

NSMutableDictionary* dict = [NSMutableDictionary dictionary];
NSValue *key = [NSValue valueWithNonretainedObject:aConnection]
/* store: */
[dict setObject:connInfo forKey:key];
/* lookup: */
[dict objectForKey:key];
Мфазекас
источник
0

Я решил создать подкласс NSURLConnection и добавить тег, делегат и NSMutabaleData. У меня есть класс DataController, который обрабатывает все управление данными, включая запросы. Я создал протокол DataControllerDelegate, чтобы отдельные представления / объекты могли прослушивать DataController, чтобы узнать, когда их запросы были завершены, и, если необходимо, сколько было загружено или ошибок. Класс DataController может использовать подкласс NSURLConnection для запуска нового запроса и сохранения делегата, который хочет прослушать DataController, чтобы узнать, когда запрос завершен. Это мое рабочее решение в XCode 4.5.2 и ios 6.

Файл DataController.h, объявляющий протокол DataControllerDelegate). DataController также является одноэлементным:

@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

Ключевые методы в файле 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]];
}

И чтобы начать запрос: [[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

И 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

У каждого NSURLConnection есть атрибут хэша, вы можете различить все по этому атрибуту.

Например, мне нужно сохранить определенную информацию до и после подключения, поэтому у моего RequestManager есть NSMutableDictionary для этого.

Пример:

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

По запросу:

- (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
источник