Керування кількома асинхронними підключеннями NSURLConnection


88

У моєму класі є маса повторюваних кодів, що виглядає так:

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

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

Які дані ми повертаємо? Якщо воно містить це, зробіть те, інакше зробіть інше. Думаю, було б корисно мати можливість позначати ці асинхронні запити, подібно до того, як ви можете позначати подання ідентифікаторами.

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

Відповіді:


77

Я відстежую відповіді у CFMutableDictionaryRef, заповненому пов'язаним із ним NSURLConnection. тобто:

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

Може здатися дивним використовувати це замість NSMutableDictionary, але я це роблю, оскільки цей CFD Dictionary зберігає лише свої ключі (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

( У мене є створити новий питання тут задаю це: stackoverflow.com/questions/1192294 / ... )
PlagueHammer

3
Це не є безпечним для потоку, якщо делегат викликається з декількох потоків. Ви повинні використовувати блокування взаємного виключення для захисту структур даних. Кращим рішенням є підклас NSURLConnection та додавання відповідей та посилань на дані як змінних примірника. Я даю більш детальну відповідь, пояснюючи це на запитання Ноктюрна: stackoverflow.com/questions/1192294/…
Джеймс Уолд,

4
Aldi ... це İŞ поточно при умови , ви починаєте все з'єднання з однією і тією ж нитки (які ви можете зробити легко, викликаючи свій метод підключення запуску з допомогою performSelector: onThread: withObject: waitUntilDone :). Поміщення всіх з'єднань у NSOperationQueue має різні проблеми, якщо ви намагаєтеся запустити більше з'єднань, ніж максимальна кількість одночасних операцій черги (операції потрапляють у чергу, а не виконуються одночасно). NSOperationQueue добре працює для операцій, пов'язаних з процесором, але для операцій, пов'язаних з мережею, вам краще використовувати підхід, який не використовує пул потоків фіксованого розміру.
Метт Галлахер,

1
Просто хотів поділитися, що для iOS 6.0 і новіших версій ви можете використовувати [NSMapTable weakToStrongObjectsMapTable]замість a CFMutableDictionaryRefта заощадити клопоти. У мене добре вийшло.
Shay Aviv,

19

У мене є проект, де у мене є два різних NSURLConnections, і я хотів використовувати того самого делегата. Я створив два властивості у своєму класі, по одному для кожного з’єднання. Потім у методі делегування я перевіряю, чи це зв’язок


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

Це також дозволяє мені скасувати конкретне підключення за іменем, коли це потрібно.


будьте обережні, це проблематично, оскільки у нього будуть умови перегонів
adit

Як ви в першу чергу призначаєте імена (saveConnection і sharingReturnedData) для кожного з’єднання?
jsherk

@adit, ні, для цього коду немає жодної умови перегони. Вам потрібно було б далеко відійти від коду створення з'єднання, щоб створити умову перегонів
Майк Абдулла,

ваше "рішення" - це саме те, чого хоче уникнути оригінальне запитання, цитуючи згори: "... багато розгалужень і потворного коду починає формулюватися ..."
stefanB

1
@adit Чому це призведе до стану перегонів? Для мене це нова концепція.
guptron

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, NSURLConnectionвикористання.

Після встановлення 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

Один із підходів, який я використав, - не використовувати той самий об’єкт, що і делегат, для кожного з’єднання. Натомість я створюю новий екземпляр мого класу синтаксичного аналізу для кожного з’єднання, яке запускається, і встановлюю делегата на цей екземпляр.


Набагато краща інкапсуляція щодо одного з'єднання.
Кедар Паранджапе


2

Зазвичай я створюю масив словників. Кожен словник має трохи ідентифікаційної інформації, об’єкт NSMutableData для зберігання відповіді та саме підключення. Коли метод делегата зв’язку спрацьовує, я шукаю словник зв’язку та обробляю його відповідно.


Бен, чи не можна було б попросити у вас зразок коду? Я намагаюся уявити, як ви це робите, але не все там.
Coocoo4Cocoa

Зокрема, Бен, як ти шукаєш словник? Ви не можете мати словник словників, оскільки NSURLConnection не реалізує NSCopying (тому його не можна використовувати як ключ).
Адам Ернст

Мат має чудове рішення нижче, використовуючи CFMutableDictionary, але я використовую безліч словників. Для пошуку потрібна ітерація. Це не найефективніше, але досить швидко.
Бен Готліб

2

Одним із варіантів є просто підклас NSURLConnection самостійно і додати -tag або подібний метод. Дизайн NSURLConnection навмисно дуже оголений, тому це цілком прийнятно.

Або, можливо, ви можете створити клас MyURLConnectionController, який відповідає за створення та збір даних про з'єднання. Тоді йому потрібно буде повідомити ваш основний об'єкт контролера лише після завершення завантаження.


2

в iOS5 і вище ви можете просто використовувати метод класу sendAsynchronousRequest:queue:completionHandler:

Не потрібно відстежувати з'єднання, оскільки відповідь повертається в обробнику завершення.


1

Мені подобається ASIHTTPRequest .


Мені дуже подобається реалізація "блоків" в ASIHTTPRequest - це як ананімні внутрішні типи в Java. Це перевершує всі інші рішення з точки зору чистоти коду та організації.
Matt Lyons

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];
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.