iOS: NSOperationQueue: Secondary queue - ios

I would like to receive some data to the server in an asynchronous way and avoiding to overload the App UI performance. Hence would like to send tasks to the secondary queue and not the main one.
This is my current solution which uses the "main queue" ([NSOperationQueue mainQueue] which I understand slows down the performance):
-(NSDictionary*) fetchURL:(NSString*)url
{
NSURLRequest *request = [[NSURLRequest alloc] initWithURL:[NSURL URLWithString:kCONTACTSINFOURL]];
__block BOOL hasError = FALSE;
__block NSDictionary *json;
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError)
{
//Verify type of connection error
json = [NSJSONSerialization JSONObjectWithData:data
options:0
error:nil];
NSLog(#"Async JSON: %#", json);
}];
if (hasError) {
[[NSOperationQueue mainQueue] suspend];
return nil;
}
return json;
}
In order to use the secondary queue, and avoid overloading the UI and App performance, is it ok to allocate a shared NSOperationQueue and refer to that? Or is there some other "better" class or method to achieve this?
This would be my improved solution using a secondary NSOperationQueue:
Creating a secondary queue:
NSOperationQueue* otherQueue = [NSOperationQueue init];
Using the other (secondary) queue:
....
[NSURLConnection sendAsynchronousRequest:request
queue:otherQueue
completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError)
....
}
Is this correct? Or is there any other way to deal with this?

... following on from the comments...
The NSURLConnection method
[NSURLConnection sendAsynchronousRequest:queue:completionHandler:]
allows you to specify the queue that the completion handler will be called on when the connection is complete. If you suspend this queue you will not stop the requests being sent, you will just stop getting callbacks when they are done. You may want to think about doing this slightly differently...
NSOperation is great, but I tend to prefer going straight for GCD (NSOperation is just a nice obj-c wrapper on the top) if you are hard-set on using NSOperations let me know and i'll add some advice for that.
I assume that you have some kind of manager class that handles all of your server communication? If not, I would recommend that you do, and have it as a singleton.
# interface ChatManager : NSObject
+ (ChatManager *)sharedManager;
#end
#implementation ChatManager {
dispatch_queue_t fetchQueue;
dispatch_queue_t postQueue;
}
+ (ChatManager *)sharedManager {
// This is just the standard apple pattern for creating a singleton
static SCAddressBookManager *sharedManager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedManager = [[SCAddressBookManager alloc] init];
});
return sharedManager;
}
- (id)init {
self = [super init];
if (self) {
// Create a dispatch queue that will run requests one after another, you could make this concurrent but that may cause messages to be lost when you suspend
fetchQueue = dispatch_queue_create("fetch_queue", DISPATCH_QUEUE_SERIAL);
postQueue = dispatch_queue_create("post_queue", DISPATCH_QUEUE_SERIAL);
}
}
- (NSDictionary *)fetchURL:(NSURL *)url {
// In here we will dispatch to our queue and so that the
dispatch_async(fetchQueue, ^{
NSURLRequest = // create your request
NSURLResponse *response = nil;
NSError *error = nil;
NSData *data = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
// Check for error
if (error) dispatch_suspend(fetchQueue); // this suspends the queue that is making the calls to the server, so will stop attempting to send messages when you have an error - you should start pinging to see when you come back online here too, and then use dispatch_resume(fetchQueue) to get it going again!
else {
json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(#"Async JSON: %#", json);
return json; // ??
}
});
}
- (NSDictionary *)postURL:(NSURL *)url data:(NSData *)bodyData {
// In here we will dispatch to our queue and so that the
dispatch_async(postQueue, ^{
/*
Make your post request.
*/
// Check for error
if (error) dispatch_suspend(postQueue);
else {
json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSLog(#"Async JSON: %#", json);
return json; // ??
}
});
}
#end
You may also need to use
dispatch_sync(dispatch_get_main_queue(), ^{
// Do stuff on the main thread here
};
if you need to make delegate style callbacks to the main thread!
This is a basic idea on how I would have things setup to try and achieve what you are going for... feel free to let me know if any of this doesn't make sense to you..?
As a side note, I have assumed that for whatever reason you need to have a simple http interface for your server. The much better approach would be to have a persistent socket open between the app and your server, and then you can push data up and down at will. A socket with a heartbeat would also let you know when your connection has gone down. Not sure if maybe you would like me to elaborate on this option some more...

Related

Objective-C: Wait to execute 'While' loop until NSURLConnection request is complete

Basically I want a way to issue a NSURLRequest multiple times in a loop until a certain condition has been met. I am using a rest api but the rest api only allows up to a maximum of 1,000 results at a time. So if i have, lets say 1,500 total, i want to make a request to get the first 1,000 then i need to get the rest with another almost exact request , except the startAt: parameter is different(so i could go from 1001 - 1500. I want to set this up in a while loop(while i am done loading all the data) and am just reading about semaphores but its not working out like I expected it to. I don't know how many results I have until i make the first request. It could be 50, 1000, or 10,000.
here is the code:
while(!finishedLoadingAllData){
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
NSURLRequest *myRequest = [self loadData: startAt:startAt maxResults:maxResults];
[NSURLConnection sendAsynchronousRequest:myRequest
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if(error){
completionHandler(issuesWithProjectData, error);
}
else{
NSDictionary *issuesDictionary = [[NSDictionary alloc] initWithDictionary:[NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableContainers error:&error]];
[issuesWithProjectData addObjectsFromArray:issuesDictionary[#"issues"]];
if(issuesWithProjectData.count == [issuesDictionary[#"total"] integerValue]){
completionHandler([issuesWithProjectData copy], error);
finishedLoadingAllData = YES;
}
else{
startAt = maxResults + 1;
maxResults = maxResults + 1000;
}
}
dispatch_semaphore_signal(semaphore);
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
Basically I want to keep the while loop waiting until the completion block finished. Then and only then do i want the while loop to check if we have all of the data or not(and if not, make another request with the updated startAt value/maxResults value.
Right now it just hangs on dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
What am i doing wrong or what do i need to do? Maybe semaphores are the wrong solution. thanks.
Ok. The more I look, the more I don't think its a bad idea to have semaphores to solve this problem, since the other way would be to have a serial queue, etc. and this solution isn't all that more complicated.
The problem is, you are requesting the completion handler to be run on the main thread
[NSURLConnection sendAsynchronousRequest:myRequest
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
and you are probably creating the NSURL request in the main thread. Hence while it waits for the semaphore to be released on the mainthread, the NSURL completion handler is waiting for the mainthread to be free of its current run loop. So create a new operation queue.
would it not be easier to do something like this instead:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ //run on a background thread
while(!finishedLoadingAllData){
NSURLRequest *myRequest = [self loadData: startAt:startAt maxResults:maxResults];
NSHTTPURLResponse *response = nil;
NSError *error = nil;
NSData *responseData = [NSURLConnection sendSynchronousRequest:myRequest returningResponse:&response error:&error]; //blocks until completed
if(response.statusCode == 200 && responseData != nil){ //handle response and set finishedLoadingAllData when you want
//do stuff with response
dispatch_sync(dispatch_get_main_queue(), ^{
//do stuff on the main thread that needs to be done
}
}
});
Please dont do that.. NSURLConnection sendAsynchronousRequest will be loading itself in loop for you, if your data is in chunk.. try this instead..
__block NSMutableData *fragmentData = [NSMutableData data];
[[NSOperationQueue mainQueue] cancelAllOperations];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
[fragmentData appendData:data];
if ([data length] == 0 && error == nil)
{
NSLog(#"No response from server");
}
else if (error != nil && error.code == NSURLErrorTimedOut)
{
NSLog(#"Request time out");
}
else if (error != nil)
{
NSLog(#"Unexpected error occur: %#", error.localizedDescription);
}
else if ([data length] > 0 && error == nil)
{
if ([fragmentData length] == [response expectedContentLength])
{
// finished loading all your data
}
}
}];
I've created two chunky json response from server handling method.. And one of them is this, so hope this will be useful to you as well.. Cheers!! ;)

How to run a NSURLConnection in background in iOS

I am working with an app which is todo list organizer, where user adds notes. I am using coredata DB to store the notes. As I am providing sync feature, I am parsing JSON data to server, and also getting JSON data from server.
I am using NSURLConnection API and its delegate functions
- (void)pushData
{
loop through the notes array and send notes 1 by one
[[request setValue:#"application/json;charset=utf-8" forHTTPHeaderField:#"Content-Type"];
[request setHTTPMethod:#"POST"];
[request setHTTPBody:jsonData];
m_dataPush = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES];
[m_dataPush start];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
Process response from server, save to core DB
and again pushData if any modified and again process the response
}
I call this API, on appEnterBackground and appBecomeActive, because, I want the data to updated on multiple devices.
The problems, which I am facing is that
1) When the notes are more, app is getting stuck, when we exit and open the app and start adding notes.
2) I tried using GCD, but then my NSURLConnection doesnot send me any response
Regards
Ranjit
Ranjit: Based on your comments in the different responses, I suspect you are sending the 1st request from the main thread. When you receive the 1st response, you process it in the background, and then send the 2nd request also from the background. The subsequent requests should be sent from the main thread
[self performSelectorOnMainThread:#selector(myMethodToOpenConnection:)
withObject:myObject
waitUntilDone:NO];
otherwise the thread exits before the delegate is called
You can use NSOperation Queue with NSURLConnection like this
//allocate a new operation queue
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
//Loads the data for a URL request and executes a handler block on an
//operation queue when the request completes or fails.
[NSURLConnection
sendAsynchronousRequest:urlRequest
queue:queue
completionHandler:^(NSURLResponse *response,
NSData *data,
NSError *error) {
if ([data length] >0 && error == nil){
//process the JSON response
//use the main queue so that we can interact with the screen
NSString *myData = [[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding];
NSLog(#"JSON data = %#", myData);
NSDictionary *myDict = [myData JSONValue];
}
}];
it will do all the processing in the background.
NSURLConnection provides a convenience method called sendAsynchronousRequest: completionHandler: that does the GCD work for you. You can tell it to run the completion handler on the main thread.
Using it, your code would get simpler as follows:
// place a declaration in your .h to make it public
- (void)pushDataWithCompletion:(void (^)(BOOL, NSError*))completion;
- (void)pushDataWithCompletion:(void (^)(BOOL, NSError*))completion
{
// setup your connection request...
[[request setValue:#"application/json;charset=utf-8" forHTTPHeaderField:#"Content-Type"];
[request setHTTPMethod:#"POST"];
[request setHTTPBody:jsonData];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
// whatever you do on the connectionDidFinishLoading
// delegate can be moved here
if (!error) {
// did finish logic here, then tell the caller you are done with success
completion(YES, nil);
} else {
// otherwise, you are done with an error
completion(NO, error);
}
}];
}
Exactly what you pass back in the block depends on what the callers care about. It's common to make some aspect of the data you collected one of the block params.
EDIT - I left out the pointer notation (*) after NSError above.
Also, say you have an array of objects that needs to be processed by the server. This method is good for one call. To handle several, lets give it a parameter. Say that each note is an NSString *;
- (void)pushNote:(NSString *)note withCompletion:(void (^)(BOOL, NSError*))completion {
// Code is the same except it forms the request body using the note parameter.
}
If the real task is to do work for several notes, you need a method that calls this one repeatedly, then tells its caller that its done.
- (void)pushNotes:(NSArray *)notes withCompletion:(void (^)(BOOL, NSError*))completion {
// if there are no more notes, we are done
if (!notes.count) return completion(YES, nil);
NSString *nextNote = notes[0];
NSArray *remainingNotes = [notes subarrayWithRange:NSMakeRange(1, notes.count-1)];
[self pushNote:nextNote withCompletion:^(BOOL success, NSError*error) {
// if success, do the rest, or else stop and tell the caller
if (success) {
[self pushNotes:remainingNotes withCompletion:completion];
} else {
completion(NO, error);
}
}];
}

NSURLConnection async not working

I'm trying to make an asynchronous NSURL Request, but I'm getting all "FALSE."
-(BOOL)checkConnectionForHost:(NSString*)host{
BOOL __block isOnline = NO;
NSURLRequest *request = [[NSURLRequest alloc]initWithURL:[NSURL URLWithString:host] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:1];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if([(NSHTTPURLResponse*)response statusCode]==200){
isOnline = TRUE;
}
}];
NSLog(#"%i",isOnline);
return isOnline;
}
Also, this code is being called "6" times when I'm actually just using it with a:
-(UICollectionViewCell*)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
and there are only 3 cells, or 3 items in my data source. First time dealing with async and callbacks in Objective-C, so a detailed answer would be much appreciated! Thanks!
Asynchronous calls will be executed in parallel, and its result will receive in the completion block. In your case, the return statement will be executed before the completion of the Asynchronous request. That will be always FALSE.
You should use Synchronous request for this, and handle not to Block the UI.
-(BOOL)checkConnectionForHost:(NSString*)host{
BOOL isOnline = NO;
NSURLRequest *request = [[NSURLRequest alloc]initWithURL:[NSURL URLWithString:host] cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:1];
NSHTTPURLResponse *response;
NSError *error;
NSData *responseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&response error:&error];
NSLog(#"Response status Code : %d",response.statusCode);
isOnline = response.statusCode == 200;
return isOnline;
}
You can use that method inside dispatch queues,
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul);
dispatch_async(queue, ^{
BOOL status = [self checkConnectionForHost:#"http://google.com"];
NSLog(#"Host status : %#",status ? #"Online" : #"Offline");
});
You should realize that this problem is inherently asynchronous. You can't solve it with a synchronous approach. That is, your accepted solution is just an elaborated and suboptimal wrapper which ends up being eventually asynchronous anyway.
The better approach is to use an asynchronous method with a completion handler, e.g.:
typedef void (^completion_t)(BOOL isReachable);
-(void)checkConnectionForHost:(NSString*)host completion:(completion_t)completionHandler;
You can implement is as follows (even though the request isn't optimal for checking reachability):
-(void)checkConnectionForHost:(NSString*)host
completion:(completion_t)completionHandler
{
NSURLRequest* request = [[NSURLRequest alloc]initWithURL:[NSURL URLWithString:host]];
[NSURLConnection sendAsynchronousRequest:request queue:[[NSOperationQueue alloc] init] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (completionHandler) {
completionHandler(connectionError == nil && [(NSHTTPURLResponse*)response statusCode]==200);
}
}];
}
Please note:
Don't set a timeout as short as in your original code.
The completion handler will be called on a private thread.
Usage:
[self checkConnectionForHost:self.host completion:^(BOOL isReachable){
dispatch_async(dispatch_get_main_queue(), ^{
self.reachableLabel.text = isReachable ? #"" : #"Service unavailable";
});
}];
Your isOnline is probably being set to YES, but it's happening asynchronously. It is almost certainly executing after you log out the value of isOnline. So you should move your NSLog() call up into the block you pass as the handler to the asynchronous URL request.

Adding UIActivityIndicatorView on Synchronous Web Service data and Populating UITableView

I am fetching the data from a web service by synchronous method. I make the request to the web service then view freezes. I try to add the UIActivityIndicatorView before loading the data from the web service and stopped it after getting the data but activity indicator is not displayed.
I tried to put the web service data fetch operations on the different thread
[NSThread detachNewThreadSelector:#selector(fetchRequest) toTarget:self withObject:nil];
but at this time TableView crashes as it does not get the data for drawing the cells.
in fetchRequest function I am doing
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL
URLWithString:URLString]];
NSData *response = [NSURLConnection sendSynchronousRequest:request
returningResponse:nil error:nil];
NSError *jsonParsingError = nil;
NSDictionary *tableData = [NSJSONSerialization JSONObjectWithData:response
options:0
error:&jsonParsingError];
responseArray = [[NSMutableArray alloc]initWithArray:[tableData objectForKey:#"data"]];
for(int i = 0; i < responseArray.count; i++)
{
NSArray * tempArray = responseArray[i];
responseArray[i] = [tempArray mutableCopy];
}
This responseArray is used to fill the information in the cell
Please tell me how to do this. Any help will be appreciated ...
The problem lies in your very approach. Synchronous methods run on the main thread. And because the UI updates on the main thread, your app hangs.
So, the solution would be using an asynchronous method to download the data on a separate thread, so that your UI won't hang.
So, use the NSURLConnection's sendAsynchronousRequest. Here's some sample code :
NSURL *url = [NSURL URLWithString:#"YOUR_URL_HERE"];
NSURLRequest *urlRequest = [NSURLRequest requestWithURL:url];
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[NSURLConnection sendAsynchronousRequest:urlRequest queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
{
//this is called once the download or whatever completes. So you can choose to populate the TableView or stopping the IndicatorView from a method call to an asynchronous method to do so.
}];
You should better use Grand Central Dispatch to fetch the data like this so you dispatch it in a background queue and do not block the main thread which is also used for UI updates:
dispatch_queue_t myqueue = dispatch_queue_create("myqueue", NULL);
dispatch_async(myqueue, ^(void) {
[self fetchRequest];
dispatch_async(dispatch_get_main_queue(), ^{
// Update UI on main queue
[self.tableView reloadData];
});
});
Regarding the Activity indicator you can use in the start of the parsing:
[self.activityIndicator startAnimating];
self.activityIndicator.hidesWhenStopped = YES
And then when your table is filled with data:
[self.activityIndicator stopAnimating];

Tracking Asynchronous Connections with Blocks

I am doing a lot of URL requests (about 60 small images) and I have started to do them Asynchronously. My code adds another image (little downloading thing) and then sets a Request going.
When the request is done I want "data" to be put in the location which was originally added for it, however, I can not see how to pass "imageLocation" to the block for it to store the image in the correct location.
I have replaced the 3rd line with below which seems to work but I am not 100% it is correct (it is very hard to tell as the images are nearly identical). I am also thinking that it is possible to pass "imageLocation" at the point where the block is declared.
Can any confirm any of this?
__block int imageLocation = [allImages count] - 1;
// Add another image to MArray
[allImages addObject:[UIImage imageNamed:#"downloading.png"]];
imageLocation = [allImages count] - 1;
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
[request setTimeoutInterval: 10.0];
[NSURLConnection sendAsynchronousRequest:request
queue:[NSOperationQueue currentQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (data != nil && error == nil)
{
//All Worked
[allImages replaceObjectAtIndex:imageLocation withObject:[UIImage imageWithData:data]];
}
else
{
// There was an error, alert the user
[allImages replaceObjectAtIndex:imageLocation withObject:[UIImage imageNamed:#"error.png"]];
}];
Dealing with asynchronous methods is a pain ;)
In your case its guaranteed that the completion block will execute on the specified queue. However, you need to ensure that the queue has a max concurrent operations count of 1, otherwise concurrent access to shared resources is not safe. That's a classic race http://en.wikipedia.org/wiki/Race_condition. The max concurrent operations of a NSOperationQueue can be set with a property.
In general, completion handlers may execute on any thread, unless otherwise specified.
Dealing with asynchronous methods gets a lot easier when using a concept called "Promises" http://en.wikipedia.org/wiki/Promise_(programming). Basically, "Promises" represent a result that will be evaluated in the future - nonetheless the promise itself is immediately available. Similar concepts are named "futures" or "deferred".
There is an implementation of a promise in Objective-C on GitHub: RXPromise. When using it you also get safe access from within the handler blocks to shared resources. An implementation would look as follows:
-(RXPromise*) fetchImageFromURL:(NSString*)urlString queue:(NSOperationQueue*) queue
{
#autoreleasepool {
RXPromise* promise = [[RXPromise alloc] init];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
[NSURLConnection sendAsynchronousRequest:request
queue:queue
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
if (data != nil) {
[promise fulfillWithValue:data];
}
else { // There was an error
[promise rejectWithReason:error];
};
}];
return promise;
}
}
Then call it:
- (void) fetchImages {
...
for (NSUInteger index = 0; index < N; ++index)
{
NSString* urlString = ...
[self fetchImageFromURL:urlString, self.queue]
.then(^id(id data){
[self.allImages replaceObjectAtIndex:index withObject:[UIImage imageWithData:data]];
return #"OK";
},
^id(NSError* error) {
[self.allImages replaceObjectAtIndex:index withObject:[UIImage imageNamed:#"error.png"]];
return error;
});
}
}
A couple of thoughts:
If you want to download 60 images, I would not advise using a serial queue for the download (e.g. do not use an operation queue with maxConcurrentOperationCount of 1), but rather use a concurrent queue. You will want to synchronize the updates to make sure your code is thread-safe (and this is easily done by dispatching the updates to a serial queue, such as the main queue, for that final update of the model), but I wouldn't suggest using a serial queue for the download itself, as that will be much slower.
If you want to use the NSURLConnection convenience methods, I'd suggest something like the following concurrent operation request approach (where, because it's on a background queue, I'm using sendSynchronousRequest rather than sendAsynchronousRequest), where I'll assume you have an NSArray, imageURLs, of NSURL objects for the URLs of your images:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4;
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(#"all done %.1f", CFAbsoluteTimeGetCurrent() - start);
NSLog(#"allImages=%#", self.allImages);
}];
[imageURLs enumerateObjectsUsingBlock:^(NSURL *url, NSUInteger idx, BOOL *stop) {
NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSURLResponse *response = nil;
NSError *error = nil;
NSData *data = [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:url] returningResponse:&response error:&error];
if (!data) {
NSLog(#"%s sendSynchronousRequest error: %#", __FUNCTION__, error);
} else {
UIImage *image = [UIImage imageWithData:data];
if (image) {
dispatch_sync(dispatch_get_main_queue(), ^{
[self.allImages replaceObjectAtIndex:idx withObject:image];
});
}
}
}];
[queue addOperation:operation];
[completionOperation addDependency:operation];
}];
[[NSOperationQueue mainQueue] addOperation:completionOperation];
A couple of asides: First, I'm using an operation queue rather than a GCD concurrent queue, because it's important to be able to constrain the degree on concurrency. Second, I've added a completion operation, because I assume it would be useful to know when all the downloads are done, but if you don't need that, the code is obviously simper. Third, that benchmarking code using CFAbsoluteTime is unnecessary, but useful solely for diagnostic purposes if you want to compare the performance using a maxConcurrentOperationCount of 4 versus 1.
Better than using the NSURLConnection convenience methods, above, you might want to use a NSOperation-based network request. You can write your own, or better, use a proven solution, like AFNetworking. That might look like:
CFAbsoluteTime start = CFAbsoluteTimeGetCurrent();
NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(#"all done %.1f", CFAbsoluteTimeGetCurrent() - start);
NSLog(#"allImages=%#", self.allImages);
}];
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.responseSerializer = [AFImageResponseSerializer serializer];
manager.operationQueue.maxConcurrentOperationCount = 4;
[imageURLs enumerateObjectsUsingBlock:^(NSURL *url, NSUInteger idx, BOOL *stop) {
NSOperation *operation = [manager GET:[url absoluteString] parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
[self.allImages replaceObjectAtIndex:idx withObject:responseObject];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"%s image request error: %#", __FUNCTION__, error);
}];
[completionOperation addDependency:operation];
}];
[[NSOperationQueue mainQueue] addOperation:completionOperation];
Because AFNetworking dispatches those completion blocks back to the main queue, that solves the synchronization issues, while still enjoying the concurrent network requests.
But the main take-home message here is that an NSOperation-based network request (or at least one that uses NSURLConnectionDataDelegatemethods) opens additional opportunities (e.g. you can cancel all of those network requests if you have to, you can get progress updates, etc.).
Frankly, having walked through two solutions that illustrate how to download the images efficiently up-front, I feel compelled to point out that this is an inherently inefficient process. I might suggest a "lazy" image loading process, that requests the images asynchronously as they're needed, in a just-in-time (a.k.a. "lazy") manner. The easiest solution for this is to use a UIImageView category, such as provided by AFNetworking or SDWebImage. (I'd use AFNetworking's if you're using AFNetworking already for other purposes, but I think that SDWebImage's UIImageView category is a little stronger.) These not only seamlessly load the images asynchronously, but offer a host of other advantages such as cacheing, more efficient memory usage, etc. And, it's as simple as:
[imageView setImageWithURL:url placeholder:[UIImage imageNamed:#"placeholder"]];
Just a few thoughts on efficiently performing network requests. I hope that helps.

Resources