What's the best approach to asynchronous image caching on the iPhone?
Asked Answered
U

4

10

I'm creating an iPhone app that will pull data down from a Web API, including email addresses. I'd like to display an image associated with each email address in table cells, so I'm searching the Address Book for images and falling back on a default if the email address isn't in the book. This works great, but I have a few concerns:

  • Performance: The recipes I've found for looking for an address book record by Email address (or phone number) are reportedly rather slow. The reason for this is that one must iterate over every address book record, and for each one that has an image, iterate over all email addresses to find a match. This can be time-consuming for a large address book, of course.

  • Table Cells: So I thought I'd gather up all the email addresses for which I need to find images and find them all at once. This way I iterate through the book only once for all addresses. But this doesn't work well for table cells, where each cell corresponds to a single email address. I'd either have to gather all the images before displaying any cells (potentially slow), or have each cell look up each image as it loads (even slower, as I'd need to iterate through the book to find a match for each email address).

  • Asynchronous Lookup: So then I thought I'd look them up in bulk, but asynchronously, using NSInvocationOperation. For each image found in AddressBook, I'd save a thumbnail in the app sandbox. Then each cell could just reference this file and show the default if it doesn't exist (because it's not in the book or hasn't yet been found). If the image is later found in the asynchronous lookup, the next time the image needs to be displayed it would suddenly appear. This might work well for periodic regeneration of images (for when images have been changed in the address book, for example). But then for any given instance of my app, an image may not actually show up for a while.

  • Asynchronous Table Cell Lookup: Ideally, I'd use something like markjnet's asynchronous table cell updating to update table cells with an image once it has been downloaded. But for this to work, I'd have to spin off an NSInvocationOperation job for each cell as it's displayed and if the cached icon is missing from the sandbox. But then we're back to inefficiently iterating through the entire address book for each one—and that can be a lot of them if you've just downloaded a whole bunch of new email addresses.

So my question is: How do others do this? I was fiddling with Tweetie2, and it looks like it updates displayed table cells asynchronously. I assume it's sending a separate HTTP request for every image it needs. If so, I imagine that searching the local address book by email address isn't any less efficient, so maybe that's the best approach? Just not worry about the performance issues associated with searching the address book?

If so, is saving a thumbnail image in the sandbox the best approach to caching? And if I wanted to create a new job to update all the thumbnails with any changes in the address book say once a day, what's the best approach to doing so?

How do the rest of you solve this sort of problem? Suggestions would be much appreciated!

Ultra answered 13/2, 2010 at 23:11 Comment(0)
N
2

Regardless of what strategy you use for the actual caching of images, I would only make one pass through the Address Book data each time you get a batch of email addresses, if possible. (And yes, I would do this asynchronously.)

Create an NSMutableDictionary which will serve as your in-memory cache for search results. Initialize this dictionary with each email address from the download as a key, with a sentinel as that key's value (such as [NSNull null]).

Next, iterate through each ABRecordRef in the Address Book, calling ABRecordCopyValue(record, kABPersonEmailProperty) and looping through the results in each ABMultiValue that is returned. If any of the email addresses are keys in your cache, set [NSNumber numberWithInt:ABRecordGetRecordId(record)] as the value of that key in your dictionary.

Using this dictionary as a lookup index, you can quickly obtain the images of ABRecordRefs for only the email addresses that you are currently displaying in your table view given the user's current scroll position, as suggested in hoopjones's answer. You can add an address book change listener to invalidate your cache, trigger another indexing operation, and then update the view, if your application needs that level of "up-to-date-ness".

Nivernais answered 15/2, 2010 at 15:22 Comment(2)
Thanks. So fare I've actually implemented a solution that looks up one address at a time, but it creates a thumbnail and saves it to the sandbox Documents directory. And yes, it only looks up the images in the address book the first time they need to be displayed. I think loading the cached image from the sandbox should be fast enough that it doesn't need to be async. I don't need the level of up-to-dateness you describe, so next I'm going to look at how to schedule regular updates of the cached thumbnails -- and yes, I will be iterating through the address book only once for all of them.Ultra
Just curious, how are you able to look up one address at a time, yet only iterate through the address book once?Nivernais
A
2

I'd use the last method you listed (Asynchronous Table Cell Lookup) but only look images for the current records being displayed. I overload the UIScrollViewDelegate methods to find out when a user has stopped scrolling, and then only start making requests for the current visible cells.

Something like this (this is slightly modified from a tutorial I found on the web which I can't find now, apologies for not citing the author) :

- (void)loadContentForVisibleCells
{
    NSArray *cells = [self.table visibleCells];
    [cells retain];
    for (int i = 0; i < [cells count]; i++) 
    { 
        // Go through each cell in the array and call its loadContent method if it responds to it.
        AddressRecordTableCell *addressTableCell = (AddressRecordTableCell *)[[cells objectAtIndex: i] retain];
        [addressTableCell loadImage];
        [addressTableCell release];
        addressTableCell = nil;
    }
    [cells release];
}


- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView; 
{
    // Method is called when the decelerating comes to a stop.
    // Pass visible cells to the cell loading function. If possible change 
    // scrollView to a pointer to your table cell to avoid compiler warnings
    [self loadContentForVisibleCells]; 
}


- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
{
    if (!decelerate) 
    {
       [self loadContentForVisibleCells]; 
    }
}

Once you know what address records are currently visible, just doing a search for those (5 -7 records probably) will be lightning fast. Once you grab the image, just cache it in a dictionary so that you don't have to redo the request for the image later.

Actinomycin answered 14/2, 2010 at 16:12 Comment(3)
Just out of curiosity, why does this sample code retain the NSArray of cells and then each AddressRecordTableCell before doing work with them and then releasing them, in loadContentForVisibleCells? I don't see what could cause them to become dealloc'd during the execution of that method.Nivernais
You're correct. It doesn't need to be retained. I wrote this code way back when I was just learning Cocoa, so there's some noob-ness in it :)Actinomycin
Good to know about UIScrollViewDelegate. That's what the LazyTableImages example linked by yonel below uses, too. The funny thing is that Tweetie 2 doesn't seem to use this approach. When I scroll fast, I can see images loading while it scrolls. It always annoyed me that Apple apps don't load images while scrolling, and Tweetie 2 seems to show that it's not necessary.Ultra
N
2

Regardless of what strategy you use for the actual caching of images, I would only make one pass through the Address Book data each time you get a batch of email addresses, if possible. (And yes, I would do this asynchronously.)

Create an NSMutableDictionary which will serve as your in-memory cache for search results. Initialize this dictionary with each email address from the download as a key, with a sentinel as that key's value (such as [NSNull null]).

Next, iterate through each ABRecordRef in the Address Book, calling ABRecordCopyValue(record, kABPersonEmailProperty) and looping through the results in each ABMultiValue that is returned. If any of the email addresses are keys in your cache, set [NSNumber numberWithInt:ABRecordGetRecordId(record)] as the value of that key in your dictionary.

Using this dictionary as a lookup index, you can quickly obtain the images of ABRecordRefs for only the email addresses that you are currently displaying in your table view given the user's current scroll position, as suggested in hoopjones's answer. You can add an address book change listener to invalidate your cache, trigger another indexing operation, and then update the view, if your application needs that level of "up-to-date-ness".

Nivernais answered 15/2, 2010 at 15:22 Comment(2)
Thanks. So fare I've actually implemented a solution that looks up one address at a time, but it creates a thumbnail and saves it to the sandbox Documents directory. And yes, it only looks up the images in the address book the first time they need to be displayed. I think loading the cached image from the sandbox should be fast enough that it doesn't need to be async. I don't need the level of up-to-dateness you describe, so next I'm going to look at how to schedule regular updates of the cached thumbnails -- and yes, I will be iterating through the address book only once for all of them.Ultra
Just curious, how are you able to look up one address at a time, yet only iterate through the address book once?Nivernais
L
1

You seem to try to implement lazy images loading in UITableView. there's a good example from Apple, I'm referencing it here : Lazy load images in UITableView

Lemniscus answered 14/2, 2010 at 20:14 Comment(1)
Many thanks for the link to the LazyTableImages example. I wasn't aware of it, and glad to have it to study.Ultra
T
0

FYI, I've released a free, powerful, and easy library for doing asynchronous image loading and fast file caching: HJ Managed Objects http://www.markj.net/asynchronous-loading-caching-images-iphone-hjobjman/

Trying answered 13/1, 2011 at 17:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.