views:

114

answers:

0

I'm trying to improve the performance of my image-intensive iPhone app by using a disk-based image cache instead of going over the network. I've modeled my image cache after SDImageCache (http://github.com/rs/SDWebImage/blob/master/SDImageCache.m), and is pretty much the same but without asynchronous cache in/out operations.

I have some scroll views and table views that load these images asynchronously. If the image is on the disk, it's loaded from the image cache, otherwise a network request is made and the subsequent result is stored in the cache.

The problem I'm running into is that as I scroll through the scroll views or table views, there's a noticeable lag as the image is loaded from disk. In particular, the animation of going from one page to another on a scroll view has a small freeze in the middle of the transition.

I've tried to fix this by:

  • Using an NSOperationQueue and NSInvocationOperation objects to make the disk access requests (in the same manner as SDImageCache), but it doesn't help with the lag at all.
  • Tweaking the scroll view controller code so that it only loads images when the scroll view is no longer scrolling. This means the disk access only fires when the scroll view stops scrolling, but if I immediately try to scroll to the next page I can notice the lag as the image loads from disk.

Is there a way to make my disk accesses perform better or have less of an effect on the UI?

Note that I'm already caching the images in memory as well. So once everything is loaded into memory, the UI is nice and responsive. But when the app starts up, or if low memory warnings are dispatched, I'll experience many of these UI lags as images are loaded from disk.

The relevant code snippets are below. I don't think I'm doing anything fancy or crazy. The lag doesn't seem to be noticeable on an iPhone 3G, but it's pretty apparent on an 2nd-gen iPod Touch.

Image caching code:

Here's a relevant snippet of my image caching code. Pretty straightforward.

- (BOOL)hasImageDataForURL:(NSString *)url {
    return [[NSFileManager defaultManager] fileExistsAtPath:[self cacheFilePathForURL:url]];
}

- (NSData *)imageDataForURL:(NSString *)url {
    NSString *filePath = [self cacheFilePathForURL:url];

    // Set file last modification date to help enforce LRU caching policy
    NSMutableDictionary *attributes = [NSMutableDictionary dictionary];
    [attributes setObject:[NSDate date] forKey:NSFileModificationDate];
    [[NSFileManager defaultManager] setAttributes:attributes ofItemAtPath:filePath error:NULL];

    return [NSData dataWithContentsOfFile:filePath];
}

- (void)storeImageData:(NSData *)data forURL:(NSString *)url {
    [[NSFileManager defaultManager] createFileAtPath:[self cacheFilePathForURL:url] contents:data attributes:nil];
}

Scroll view controller code

Here's a relevant snippet of the code that I use for displaying images in my scroll view controllers.

- (void)scrollViewDidScroll:(UIScrollView *)theScrollView {
    CGFloat pageWidth = theScrollView.frame.size.width;
    NSUInteger index = floor((theScrollView.contentOffset.x - pageWidth / 2) / pageWidth) + 1;

    [self loadImageFor:[NSNumber numberWithInt:index]];
    [self loadImageFor:[NSNumber numberWithInt:index + 1]];
    [self loadImageFor:[NSNumber numberWithInt:index - 1]];
}

- (void)loadImageFor:(NSNumber *)index {
    if ([index intValue] < 0 || [index intValue] >= [self.photoData count]) {
        return;
    }

    // ... initialize an image loader object that accesses the disk image cache or makes a network request

    UIView *iew = [self.views objectForKey:index];    
    UIImageView *imageView = (UIImageView *) [view viewWithTag:kImageViewTag];
    if (imageView.image == nil) {
        NSDictionary *photo = [self.photoData objectAtIndex:[index intValue]];
        [loader loadImage:[photo objectForKey:@"url"]];
    }
}

The image loader object is just a lightweight object that checks the disk cache and decides whether or not to fetch an image from disk or network. Once it's done, it calls a method on the scroll view controller to display the image:

- (void)imageLoadedFor:(NSNumber *)index image:(UIImage *)image {
    // Cache image in memory
    // ...

    UIView *view = [self.views objectForKey:index];
    UIImageView *imageView = (UIImageView *) [view viewWithTag:kImageViewTag];
    imageView.contentMode = UIViewContentModeScaleAspectFill;
    imageView.image = image;
}

UPDATE

I was experimenting with the app, and I disabled the image cache and reverted to always making network requests. It looks like simply using network requests to fetch images is also causing the same lag when scrolling through scroll views and table views! That is, when a network request finishes and the image is shown in the scroll view page or table cell, the UI slightly lags a bit and has a few split seconds of lag as I try to drag it.

The lag seems to be just more noticeable when using the disk cache, since the lag always occurs right at the page transition. Perhaps I'm doing something wrong when assigning the loaded image to the appropriate UIImageView?

Also - I've tried using small images (50x50 thumbnails) and the lag seems to improve. So it seems that the performance hit is due to either loading a large image from disk or loading a large image into an UIImage object. I guess one improvement would be to reduce the size of the images being loaded into the scroll view and table views, which was what I was planning to do nonetheless. However, I just don't understand how other photo-intensive apps are able to present what looks like pretty high-res photos in scrollable views without performance problems from going to disk or over the network.