Roathe.com

Where the web comes to die…

December 18th, 2009

How to background load and cache UIImageViews images

Apple, Development, featured, iPhone / iOS, by Lane Roathe.

It’s often the case that the apps I am working on are required to download quite a couple, or even a few dozen, images and display them in a UIImageView. In looking online I found a few different methods of loading image data for a UIImageView in the background, and a few examples of caching image data, but they were all overly complicated for my needs, and none that combined the two ideas.

So, I’m posting here my very simply extension of the UIImageView class that provides a way to load an image in the background. It also provides an extremely simply caching system, which works great if you are displaying a list of thumbnails but would need better memory management for a true cache.

You can download the UIImageView+Cached - Simple extension to a UIImageView that allows loading of UIImageView's from URL's in the background, and also provides a very simple memory caching system. here.

The header file is pretty straight forward, it simply defines our additional methods for the UIImageView class.

// UIImageView+Cached.h
//
// Created by Lane Roathe
// Copyright 2009 Ideas From the Deep, llc. All rights reserved.

@interface UIImageView (Cached)

-(void)loadFromURL:(NSURL*)url;
-(void)loadFromURL:(NSURL*)url afterDelay:(float)delay;

@end

Note that both methods assume you already have a UIImageView to work with. While it would be fairly easy to add a method to create a UIImageView as part of the load, I chose not to because all of the places where I wanted to use this were defined in Interface Builder (IB) and hooked up to an instance variable. I do as little interface design in code as possible, even laying out all of my table cells in IB. For me it makes development a lot faster, and interface tweeks and updates not just simpler, but requiring fewer iterations.

loadFromURL: simply takes a URL to any supported image file and sets up for loading the image in the background. When the image finishes loading the UIImageView’s setImage: method will be called to update the view’s image.

loadFromURL:afterDelay: simply delays calling loadFromURL: for the amount of time (in seconds) given as the delay. I’ve used this in programs where I wanted to optimize other parts of the program to display first, but still put the image load call in the same code section (ie, typically in viewWillDisplay in my apps).
Now for the actual code, which is quite small for both a background loader and caching system.

// UIImageView+Cached.h
//
// Created by Lane Roathe
// Copyright 2009 Ideas From the Deep, llc. All rights reserved.

#import “UIImageView+Cached.h”

#pragma mark -
#pragma mark — Threaded & Cached image loading —

@implementation UIImageView (Cached)

Note here that I have hard-coded my cache for a maximum of 50 images. Typically cache systems are limited in the amount of RAM used instead of a count, but for this quick category it fit my needs. Again, if you are loading small images (like icons or thumbnails) then this will probably work fine for you as well, otherwise you probably want to Google for a real caching solution :)

#define MAX_CACHED_IMAGES 50 // max # of images we will cache before flushing cache and starting over

I used a static variable in the cache getter to simplify the code. I don’t really like this, but it does mean that I don’t have to worry about calling an init or doing any heavier extension of UIImageView so all in all while it’s not the best solution, it does what it needs quite well.

The method simply checks to see if we have already allocated a dictionary we can use to cache our image references, and if not allocate said dictionary. The method returns the dictionary reference.

// method to return a static cache reference (ie, no need for an init method)
-(NSMutableDictionary*)cache
{

static NSMutableDictionary* _cache = nil;

if( !_cache )

_cache = [NSMutableDictionary dictionaryWithCapacity:MAX_CACHED_IMAGES];

assert(_cache);
return _cache;

}

This is the meet and potatoes of the code here. The first thing it does is look to see if the requested image is in our cache. Note that it does this by the URL ‘string’ returned by the NSObject description method. This is about as simple a check as you can do, because it does not handle any cases where the image changes on the server, there are two identical images with different URL’s, two URL’s resolve to the same image (https and http for instance), etc.

If we don’t find the image in the cache, we setup an allocation pool because we are going to assume that we are not on the main thread, which is true if either of the public methods for this extension are called. We then use the standard NSData method to load the file from the URL and pass that to UIImage to translate that data into something the system can use (ie, the UIImage). After we load a new image, we first check to see if we have available room in our cache, and if not clear the cache out (again, very simple caching here!). Then in either case we can add the image to the cache.

Finally the method calls the UIImageView’s setImage: method to update the view with the new image data. We don’t call it directly because again we are assuming that one of the public API’s were called, in which case we would not be on the main thread, and making any UI calls is therefore bad. Thus, we use the performSelectorOnMainThread: method to call setImage: so that it can be done safely.

// Loads an image from a URL, caching it for later loads
// This can be called directly, or via one of the threaded accessors
-(void)cacheFromURL:(NSURL*)url
{

UIImage* newImage = [[self cache] objectForKey:url.description];
if( !newImage )
{

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSError *err = nil;
newImage = [[UIImage imageWithData: [NSData dataWithContentsOfURL:url options:0 error:&err]] retain];
if( newImage )
{

// check to see if we should flush existing cached items before adding this new item
if( [[self cache] count] >= MAX_CACHED_IMAGES )

[[self cache] removeAllObjects];

[[self cache] setValue:newImage forKey:url.description];

}
else

NSLog( @"UIImageView:LoadImage Failed: %@", err );

[pool drain];

}

if( newImage )

[self performSelectorOnMainThread:@selector(setImage:) withObject:newImage waitUntilDone:NO];

}

And finally, the public methods. Each is very simple; loadFromURL: uses performSelectorInBackground: to call the cacheFromURL: method above on a different thread. This allows the main thread (which is running the UI and main loop of your app) to continue on uninterrupted. This slows down the loading a bit, but keeps the user interactions and interface updates nice and smooth, resulting in a better end user experience.

The loadFromURL:afterDelay: method simply uses the afterDelay version of performSelector: to call loadFromURL: after the specified delay. The only purpose here is to provide a way to organize downloads in a very simple, yet usable fashion.

For instance, let’s say I am going to present a list of home for sale, which requires downloading 20 images, and at the same time make 20 web page requests for descriptions of the homes, their prices, etc. We could wait for everything to load, but that is not good end user design (imo). Instead, I can request the UIImageView’s for those 20 homes update their images, but delay that load for 2-3 seconds. Then, I can grab the text data from the website (which typically comes down very fast), and present the view to the user. Thus the user can start reading about the properties and as images come in the view will automatically update the images the user sees.

// Methods to load and cache an image from a URL on a separate thread
-(void)loadFromURL:(NSURL *)url
{

[self performSelectorInBackground:@selector(cacheFromURL:) withObject:url];

}

-(void)loadFromURL:(NSURL*)url afterDelay:(float)delay
{

[self performSelector:@selector(loadFromURL:) withObject:url afterDelay:delay];

}
@end

OK, that’s it. In case you missed it above, you can download the UIImageView+Cached - Simple extension to a UIImageView that allows loading of UIImageView's from URL's in the background, and also provides a very simple memory caching system. here.

Hopefully this will prove helpful to someone :)
Update: Take a look at Michael Amorose’s additions to UIImageView’s if mine isn’t your cup of tea.

Back Top

Responses to “How to background load and cache UIImageViews images”

  1. very useful!!! except i stil get this error messages

    2010-05-03 17:11:32.695 base1[9629:6417] *** _NSAutoreleaseNoPool(): Object 0x1566a90 of class NSCFDictionary autoreleased with no pool in place – just leaking
    Stack: (0x305a2e6f 0×30504682 0x4d543 0x4d5be 0x3050a79d 0x3050a338 0x909c4a19 0x909c489e)
    2010-05-03 17:11:32.695 base1[9629:5a13] *** _NSAutoreleaseNoPool(): Object 0x1566dd0 of class NSCFDictionary autoreleased with no pool in place – just leaking
    Stack: (0x305a2e6f 0×30504682 0x4d543 0x4d5be 0x3050a79d 0x3050a338 0x909c4a19 0x909c489e)
    2010-05-03 17:11:32.695 base1[9629:5813] *** _NSAutoreleaseNoPool(): Object 0×1567130 of class NSCFDictionary autoreleased with no pool in place – just leaking
    Stack: (0x305a2e6f 0×30504682 0x4d543 0x4d5be 0x3050a79d 0x3050a338 0x909c4a19 0x909c489e)
    2010-05-03 17:11:32.698 base1[9629:4917] *** _NSAutoreleaseNoPool(): Object 0×1567460 of class NSCFDictionary autoreleased with no pool in place – just leaking
    Stack: (0x305a2e6f 0×30504682 0x4d543 0x4d5be 0x3050a79d 0x3050a338 0x909c4a19 0x909c489e)

    Andy Jacobs at May 3, 2010 10:16 am
  2. This is an elegant solution to the problem, easily adapted to existing code. Your approach definitely was helpful for my project.

    My only concern is (and this may be something I introduced in adapting your example), it seems like the dictionary you get from dictionaryWithCapacity: is autoreleased, and the code doesn’t retain it. I saw a situation where my app crashed because the cache was dealloc’d while in use because of this. Adding a call to retain prevents this, but also introduces a small leak as long as the program is running.

    You mention your own objections to using a static variable in an accessor, but I think this is still the most pragmatic approach since using +initialize with a static global will still introduce an object that persists until the application is exiting.

  3. Andy and Warren were both correct in pointing out that the code I uploaded has a bug where the caching dictionary is autoreleased and needs to be contained. I adapted the code from a library where the dictionary reference is a method in an object and synthesized with a retain property.

    To fix the line

    _cache = [NSMutableDictionary dictionaryWithCapacity:MAX_CACHED_IMAGES];

    to

    _cache = [[NSMutableDictionary dictionaryWithCapacity:MAX_CACHED_IMAGES] retain];

    Warren is correct when he points out that doing so will leak this object, because the cache dictionary is never released. However, since it’s a static it will only leak one dictionary and that will be released when your app exits.

    I’ll revisit this code and see if I can’t come up with a way to solve this issue without loosing the simplicity of the existing solution (ie, no classes to create, etc.).

    Thanks for the feedback guys, apologies it took so long to reply (new job and all :) ).

    -lane

  4. You do a good job explaining how you’ve built this class, but not how to actually use it..

    Maybe mention that it’s a Category that you’ve created here, so that someone new to objective-c (like myself) can then google that and find out how Categories work:

    http://macdevelopertips.com/objective-c/objective-c-categories.html

    Thanks!

  5. Hi, thanks for the code, I integrated it into my tableView and it loads the images but only those out of the current scrolled-in view. So when I scroll down the table the images are there and when I scroll back up to the top of the table, those that weren’t initially there are now in place… the top one won’t appear at all unless I do the scrolling up and down, no matter how long I wait, so it appears to be an issue with the lazy loading.

    In:
    cellForRowAtIndexPath I used the following code to call the category method:

    cell.imageView.layer.masksToBounds = YES;
    cell.imageView.layer.cornerRadius = 0;
    NSURL* aURL = [NSURL URLWithString:urlCoverMed];
    [cell.imageView loadFromURL:aURL];
    float sw=50/cell.imageView.image.size.width;
    float sh=65/cell.imageView.image.size.height;
    cell.imageView.transform=CGAffineTransformMakeScale(sw,sh);

  6. NSMutableDictionary isn’t thread safe, is it?

    seems like you’ve got a nasty race condition there – if two threads try to modify the cache at the same time – BOOM.

    no?

  7. awesome, thank you much!

  8. qarl,

    First, apologies for taking so long to reply. Had some family to take care of and, well, updating this website got backgrounded.

    Anyway, you are correct, NSMutableDictionary is not thread safe. With the code attached to this article, it’s relatively unlikely to actually be an issue, but there is that potential.

    The good news is that I have improved this category and the updated version performs just as well at downloading async yet runs on the main thread so it’s thread safe. It simply allows NSURLConnection to do it’s own threading, and all callbacks are to the main thread.

    I’ll post up the new code this weekend.

    -lane

  9. Jom,

    The issue you are seeing is due to how this version loads the data and handles the caching. An updated version will be up shortly that solves this, and many other, issues.

    -lane

  10. hows the update to foreground thread coming? just wondering since i have a use for this and would like to see how it was modified to accomodate the foreground thread. Thanks

    -b

  11. The coding is actually completed. I just need to finish writing up the article and getting the code up on github. I plan to get that completed by this weekend.

    -lane

  12. Hi Lane,

    Did you get to update the code? I like your idea, but i might do:
    1) use NSCache (ios4+) which is completely threadsafe
    2) use category extention to add strong property cache (for ARC)

    What do you think?

  13. I haven’t had a chance to clean up the update for publishing yet. And, part of the reason is that I am wanting to ditch the legacy iOS 3.x support.

    So, yes, I would think using NSCache might be the way to go here.

  14. Hi,

    This is a great tutorial. Did you upload the enhanced one?

  15. Hi there

    Have you considered updating this for iOS5 (new memory management, etc)

    Thanks!

  16. I have, but real work has slowed progress on that update considerably.

  1. No trackbacks yet.

Leave a Reply

Your email address will not be published. Required fields are marked *

*