管理多个异步 NSURLConnection 连接

我的类中有大量的重复代码,它们看起来像下面这样:

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

异步请求的问题在于,当您有不同的请求发出,并且您有一个委托将它们作为一个实体处理时,许多分支和丑陋的代码开始形成:

我们能得到什么样的数据?如果它包含这个,做那个,否则做其他。我认为能够对这些异步请求进行标记是非常有用的,就像您能够使用 ID 对视图进行标记一样。

我很好奇,对于管理处理多个异步请求的类,什么策略最有效。

43892 次浏览

I usually create an array of dictionaries. Each dictionary has a bit of identifying information, an NSMutableData object to store the response, and the connection itself. When a connection delegate method fires, I look up the connection's dictionary and handle it accordingly.

I track responses in an CFMutableDictionaryRef keyed by the NSURLConnection associated with it. i.e.:

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

It may seem odd to use this instead of NSMutableDictionary but I do it because this CFDictionary only retains its keys (the NSURLConnection) whereas NSDictionary copies its keys (and NSURLConnection doesn't support copying).

Once that's done:

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

and now I have an "info" dictionary of data for each connection that I can use to track information about the connection and the "info" dictionary already contains a mutable data object that I can use to store the reply data as it comes in.

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
NSMutableDictionary *connectionInfo =
CFDictionaryGetValue(connectionToInfoMapping, connection);
[[connectionInfo objectForKey:@"receivedData"] appendData:data];
}

One option is just to subclass NSURLConnection yourself and add a -tag or similar method. The design of NSURLConnection is intentionally very bare bones so this is perfectly acceptable.

Or perhaps you could create a MyURLConnectionController class that is responsible for creating and collecting a connection's data. It would then only have to inform your main controller object once loading is finished.

One approach I've taken is to not use the same object as the delegate for each connection. Instead, I create a new instance of my parsing class for each connection that is fired off and set the delegate to that instance.

Try my custom class, MultipleDownload, which handles all these for you.

As pointed out by other answers, you should store connectionInfo somewhere and look up them by connection.

The most natural datatype for this is NSMutableDictionary, but it cannot accept NSURLConnection as keys as connections are non copyable.

Another option for using NSURLConnections as keys in NSMutableDictionary is using NSValue valueWithNonretainedObject]:

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

THIS IS NOT A NEW ANSWER. PLEASE LET ME SHOW YOU HOW I DID

To distinguish different NSURLConnection within same class's delegate methods, I use NSMutableDictionary, to set and remove the NSURLConnection, using its (NSString *)description as key.

The object I chose for setObject:forKey is the unique URL that is used for initiating NSURLRequest, the NSURLConnection uses.

Once set NSURLConnection is evaluated at

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

I have a project where I have two distinct NSURLConnections, and wanted to use the same delegate. What I did was create two properties in my class, one for each connection. Then in the delegate method, I check to see if which connection it is


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

This also allows me to cancel a specific connection by name when needed.

I decided to subclass NSURLConnection and add a tag, delegate, and a NSMutabaleData. I have a DataController class that handles all of the data management, including the requests. I created a DataControllerDelegate protocol, so that individual views/objects can listen to the DataController to find out when their requests were finished, and if needed how much has been downloaded or errors. The DataController class can use the NSURLConnection subclass to start a new request, and save the delegate that wants to listen to the DataController to know when the request has finished. This is my working solution in XCode 4.5.2 and ios 6.

The DataController.h file that declares the DataControllerDelegate protocol). The DataController is also a 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

The key methods in the DataController.m file:

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

And to start a request: [[NSURLConnectionWithDelegate alloc] initWithRequest:request delegate:self startImmediately:YES tag:@"Login" dataDelegate:delegate];

The 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

And the 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

in iOS5 and above you can just use the class method sendAsynchronousRequest:queue:completionHandler:

No need to keep track of connections since the response returns in the completion handler.

Subclassing NSURLConnection to hold the data is clean, less code than some of the other answers, is more flexible, and requires less thought about reference management.

// 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 it as you would NSURLConnection and accumulate the data in its data property:

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

That's it.

If you want to go further you can add a block to serve as a callback with just a couple more lines of code:

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

Set it like this:

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

and invoke it when loading is finished like this:

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

You can extend the block to accept parameters or just pass the DataURLConnection as an argument to the method that needs it within the no-args block as shown

Every NSURLConnection has an hash attribute, you can discriminate all by this attribute.

For example i need to mantain certain information before and after connection, so my RequestManager have an NSMutableDictionary to do this.

An Example:

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

After request:

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