2

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

         }];
Cezar
  • 55,636
  • 19
  • 86
  • 87
Recycled Steel
  • 2,272
  • 3
  • 30
  • 35

2 Answers2

1

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;
        });  
    }
}
CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67
  • It does not recognise the type RXPromise, do I need to import something? – Recycled Steel May 28 '13 at 08:34
  • It's a single class (header and implementation file) that must be incorporated into your project. It requires ARC enabled. You can "download" the project via "git clone " or simple clicking the "ZIP" button. – CouchDeveloper May 28 '13 at 09:16
  • Found it and it looks perfect. I already have ARC enabled but it can not compile as a Macro (dispatch_release) is trying to release an object, and I can't edit it has it is locked (was just going to comment out the release). I really do not want to turn ARC off so trying to unlock (the offer xcode makes to unlock it does not work). – Recycled Steel May 28 '13 at 10:30
  • Can't unlock as it is object.h by the looks of things (an Apple file). – Recycled Steel May 28 '13 at 10:57
  • For completeness I have added a test on the response - **NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;** and then **if (data != nil && error == nil && [httpResponse statusCode] == 200)** This is because a 404 error for example is a response without errors and data. Thanks for your help @CounchDeveloper – Recycled Steel May 28 '13 at 12:50
  • Your welcome! ;) RXPromise is a single class, and if you want to use it in your project, simply copy the header and module file into you sources; it requires ARC _enabled_ ;) Should add that the RXPromise project is still in its infancy - so, please check frequently the project status - or better add yourself as a "watcher" to the project. If you have any questions, please open an issue - and I'll try to answer it as soon as possible. – CouchDeveloper May 29 '13 at 11:27
  • The issue you are describing probably occurred when building for Deployment targets >= iOS 6.0 and >= Mac OS X 10.8. This has been fixed now. – CouchDeveloper May 30 '13 at 20:40
  • Yep that seems to be the problem, I am now compiling with 5.1 as the target and seems to be fine. I have the updated version but it throws up a Parse Issue for target iOS6. Thanks again @CouchDeveloper, also for others it is [here](https://github.com/couchdeveloper/RXPromise) at GitHub. – Recycled Steel May 31 '13 at 10:41
  • Sorry @CouchDeveloper one more thing, when you call it what should be put in queue? – Recycled Steel May 31 '13 at 13:41
  • I would appreciate it if you could open an issue on GitHub and describe in detail your problem. I will fix issues asap, and can help with examples to show the basic usage. – CouchDeveloper May 31 '13 at 14:18
  • There was indeed an error when compiling the source for iOS >= 6.1. Fixed that now. I have to apologize for the inconvenience. Background: the RXPromise class is a spin-off project of a network library which has its own project and builds for Deployment targets iOS 5 and Mac OS X 10.7 - where this issue is not present. The RXPromise is still in its infancy - any comments and suggestions are appreciated! – CouchDeveloper May 31 '13 at 14:40
1

A couple of thoughts:

  1. 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.

  2. 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.

  3. 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.).

  4. 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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Nice answer! why did you use mainQueue for the completion operation for your first suggestion? is created a dependency between operations on different queue allowed? +1 for mentioning a proven solution like AFNetworking! – Guy Nov 20 '14 at 12:15
  • When you have multiple threads updating/accessing some model object, you generally want to synchronize this interaction to make it thread-safe. There are tons of options here (locks, `@synchronized` directive, reader-writer pattern, etc.), but dispatching model interaction to serial queue is simple solution (and main queue is very useful in this case). If doing something more demanding I might suggest different approach, but main queue is quite effective in this case. – Rob Nov 20 '14 at 12:29