5

I am trying to write an iPad app that loads an image from a URL. I am using the following image loading code:

    url = [NSURL URLWithString:theURLString];
    NSData *data = [NSData dataWithContentsOfURL:url];
    img = [[UIImage alloc] initWithData:data];
    [imageView setImage:img];
    [img release];
    NSLog(@"Image reloaded");

All of that code gets added to a NSOperationQueue as an operation so it will load asynchronously and not cause my app to lock up if the image's websever is slow. I added the NSLog line so I could see in the console when this code finished executing.

I have noticed consistently that the image is updated in my app about 5 seconds AFTER the code finishes executing. However if I use this code on it's own without putting it in the NSOperationQUeue it seems to update the image almost immediately.

The lag is not caused entirely by a slow web server... I can load the image URL in Safari and it takes less than a second to load, or I can load it with the same code without the NSOperationQueue and it loads much more quickly.

Is there any way to reduce the lag before my image is displayed but keep using a NSOperationQueue?

Jackson
  • 3,555
  • 3
  • 34
  • 50

2 Answers2

6

According to the documentation, the code you have written is invalid. UIKit objects may not be called anywhere but on the main thread. I'll bet that what you're doing happens to work in most respects but doesn't successfully alter the display, with the screen being updated by coincidence for some other reason.

Apple strongly recommend that threads are not the way to perform asynchronous URL fetches if you want to remain battery efficient. Instead you should be using NSURLConnection and allowing the runloop to organise asynchronous behaviour. It's not that hard to write a quick method that just accumulates data to an NSData as it comes then posts the whole thing on to a delegate when the connection is complete but assuming you'd rather stick with what you've got I'd recommend:

url = [NSURL URLWithString:theURLString];
NSData *data = [NSData dataWithContentsOfURL:url];
[self performSelectorOnMainThread:@selector(setImageViewImage:) withObject:data waitUntilDone:YES];

...

- (void)setImageViewImage:(NSData *)data
{
    img = [[UIImage alloc] initWithData:data];
    [imageView setImage:img];
    [img release];
    NSLog(@"Image reloaded");
}

performSelectorOnMainThread does what the name says — the object is sent to will schedule the selector requested with the object given as a single parameter on the main thread as soon as the run loop can get to it. In this case 'data' is an autoreleased object on the pool in the thread implicitly created by the NSOperation. Because you need it to remain valid until it has been used, I've used waitUntilDone:YES. An alternative would be to make data something that you explicitly own and have the main thread method release it.

The main disadvantage of this method is that if the image returns in a compressed form (such as a JPEG or a PNG), it'll be decompressed on the main thread. To avoid that without making empirical guesses about the behaviour of UIImage that go above and beyond what is documented to be safe, you'd need to drop to the C level and use CoreGraphics. But I'm taking it as given that doing so is beyond the scope of this question.

Tommy
  • 99,986
  • 12
  • 185
  • 204
  • Thanks, Tommy! I'll have a look at it tonight and see where I can get from what you've told me. Actually, this morning I was reading up different ways of downloading images in Coacoa, and I discovered on my own that when I re-wrote the whole image handling code using NSUrlRequest and NSURLConnection, it seemed to load the image as I expected. I'm still not sure whether I will use the new method I wrote or the code you just showed me but having options is great, and writing it both ways was a good learning experience. Thanks again for your help and thanks for putting up with a newbie like me!:) – Jackson Nov 30 '10 at 21:58
  • By the way, if anybody else reads this question, I found this article very helpful in implimenting a NSUrlRequest based image loader. http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/URLLoadingSystem/Tasks/UsingNSURLConnection.html – Jackson Nov 30 '10 at 22:00
1

Tommy is correct about needing to do all UIKit stuff on the main thread. However, if you're running the fetch on a background operation queue, there's no need to use the NSURLConnection asynchronous loading. Also, by keeping the image decoding work on the background operation, you'll keep the main thread from blocking while decoding the image.

You should be able to use your original code as is, but just change [imgView setImage:img] to:

[imageView performSelectorOnMainThread:@selector(setImage:)
                          withObject:img
                       waitUntilDone:NO];
Daniel Dickison
  • 21,832
  • 13
  • 69
  • 89
  • Wow, that fixed the problem. Thanks so much! That was so simple! I'm still not entirely sure why though. I will have to read up on it. Now that I've got a fully implemented and working NSUrlConnection method (which on it's own is asynchronous) and a NSData/dataWithContentsOfURL method using NSObjectQueue for asynchronicity method which works, which one do you recommend I use? What are the strengths of using NSUrlConnection vs. NSData and vice versa? I have heard that NSUrlConnection has more flexibility with regards to caching and handles errors better. Is that true? Thanks again! – Jackson Dec 01 '10 at 06:26
  • It's true that NSURLConnection gives you more flexibility. But using the NSURLConnection's asynchronous loading in a background NSOperation is quite tricky, because you'll need to set up your own run loop. You can use `-[NSURLConnection sendSynchronousRequest:returningResponse:error:]` method to get some of the flexibility offered by NSURLConnection (see NSMutableURLRequest) while keeping things synchronous. That's probably what I'd do unless you have complex HTTP redirect/authentication/caching requirements. – Daniel Dickison Dec 01 '10 at 14:57
  • As for "why" this solution works is that the view drawing code running on the main thread needs to know that you just changed the image of the image view. By calling setImage: in a background thread, presumably the main thread is never notified that there's new content to be drawn (until something else causes the view to be refreshed). It's possible you can get much worse behavior by doing this on the background thread -- e.g. if you change out the image while the main thread is in the middle of trying to draw it. – Daniel Dickison Dec 01 '10 at 15:01
  • Just a clarification - my NSData method uses NSOperationQueue (not NSObjectQueue... oops). However, my NSUrlRequest method does not use NSObjectQueue and it is my understanding that it is by default asynchronous. At least it runs without holding up my app until it finishes executing. Maybe my understanding of what is synchronous vs. asynchronous is limited though. The Apple document I based my code on mentions that if you want to make your code synchronous you need to use a special function, which makes me think it is somehow executed asynchronously by default. – Jackson Dec 02 '10 at 03:49
  • Oops -- I meant NSOperationQueue (there's no such thing as NSObjectQueue, as far as I know). See my earlier comment about how to use NSURLConnection to do the request synchronously (it's a class method though -- the `-` is a typo). – Daniel Dickison Dec 02 '10 at 13:42
  • Haha I was confused as well. First I wrote NSOperationQueue then NSObjectQueue but what I think I meant was NSOperationQueue. – Jackson Dec 05 '10 at 14:42