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:
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.
You should check out the LazyTableImages sample app.
If you've narrowed it down to network activity I would try encapsulating your request to ensure it is 100% off of the main thread. While you can use NSURLConnection asynchronously and respond to it's delegate methods, I find it easier to wrap a synchronous request in a background operation. You can use NSOperation or grand central dispatch if your needs are more complex. An (relatively) simple example in an imageLoader implementation could be:
// imageLoader.m
// assumes that that imageCache uses kvp to look for images
- (UIImage *)imageForKey:(NSString *)key
{
// check if we already have the image in memory
UImage *image = [_images objectForKey:key];
// if we don't have an image:
// 1) start a background task to load an image from a file or URL
// 2) return a default image to display while loading
if (!image) {
[self performSelectorInBackground:@selector(loadImageForKey) withObject:key];
image = [self defaultImage];
}
return image;
}
- (void)loadImageForKey:(NSString *)key
{
NSAutoReleasePool *pool = [[NSAutoReleasePool alloc] init];
// attempt to load the image from the file cache
UIImage *image = [self imageFromFileForKey:key];
// if no image, load the image from the URL
if (!image) {
image = [self imageFromURLForKey:key];
}
// if no image, return default or imageNotFound image
if (!image) {
image = [self notFoundImage];
}
if ([_delegate respondsTo:@selector(imageLoader:didLoadImage:ForKey:)]) {
[_delegate imageLoader:self didLoadImage:image forKey:key];
}
[pool release];
}
- (UIImage *)imageFromURLForKey:(NSString *)key
{
NSError *error = nil;
NSData *imageData = [NSData dataWithContentsOfURL:[self imageURLForKey:key]
options:0
error:&error];
UIImage *image;
// handle error if necessary
if (error) {
image = [self errorImage];
}
// create image from data
else {
image = [UIImage imageWithData:imageData];
}
return image;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With