从磁盘加载图像时滚动视图和表视图性能

时间:2010-09-21 02:14:42

标签: iphone image uitableview caching uiscrollview

我正在尝试通过使用基于磁盘的图像缓存而不是通过网络来提高我的图像密集型iPhone应用程序的性能。我在SDImageCache(http://github.com/rs/SDWebImage/blob/master/SDImageCache.m)之后对我的图像缓存进行了建模,并且几乎相同但没有异步缓存进/出操作。

我有一些滚动视图和表视图,它们异步加载这些图像。如果映像在磁盘上,则从映像缓存加载,否则发出网络请求,后续结果存储在缓存中。

我遇到的问题是,当我滚动浏览滚动视图或表视图时,从磁盘加载图像时会出现明显的延迟。特别是,在滚动视图中从一个页面转到另一个页面的动画在转换过程中有一个小的冻结。

我试图解决这个问题:

  • 使用NSOperationQueue和NSInvocationOperation对象来发出磁盘访问请求(与SDImageCache一样),但它根本没有帮助。
  • 调整滚动视图控制器代码,使其仅在滚动视图不再滚动时加载图像。这意味着磁盘访问仅在滚动视图停止滚动时触发,但如果我立即尝试滚动到下一页,我会注意到图像从磁盘加载时的延迟。

有没有办法让我的磁盘访问效果更好或对用户界面的影响更小?

请注意,我已经在内存中缓存了这些图像。因此,一旦将所有内容加载到内存中,UI就会很好并且响应迅速。但是当应用程序启动时,或者如果调度了低内存警告,我将会遇到许多这些UI滞后,因为图像是从磁盘加载的。

相关的代码段如下。我不认为我做任何花哨或疯狂的事情。 iPhone 3G上的延迟似乎并不明显,但在第二代iPod Touch上却非常明显。

图片缓存代码:

这是我的图片缓存代码的相关摘要。很简单。

- (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];
}

滚动视图控制器代码

这是我用于在滚动视图控制器中显示图像的相关代码片段。

- (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"]];
    }
}

图像加载器对象只是一个轻量级对象,它检查磁盘缓存并决定是否从磁盘或网络获取映像。完成后,它会调用滚动视图控制器上的方法来显示图像:

- (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;
}

更新

我正在尝试使用该应用,我禁用了图像缓存并恢复为始终发出网络请求。在滚动浏览滚动视图和表视图时,看起来简单地使用网络请求来获取图像也会导致相同的延迟!也就是说,当网络请求完成并且图像显示在滚动视图页面或表格单元格中时,当我尝试拖动它时,UI略微滞后并且有几分钟的延迟。

使用磁盘缓存时滞后似乎更明显,因为滞后总是发生在页面转换时。将加载的图像分配给适当的UIImageView时,我可能做错了吗?

另外 - 我尝试过使用小图片(50x50缩略图)并且延迟似乎有所改善。因此,性能影响似乎是由于从磁盘加载大图像或将大图像加载到UIImage对象中。我想一个改进是减少加载到滚动视图和表视图中的图像的大小,这是我计划做的事情。但是,我只是不明白其他照片密集型应用程序如何能够在可滚动视图中呈现看起来非常高分辨率的照片,而不会出现进入磁盘或通过网络出现性能问题。

5 个答案:

答案 0 :(得分:1)

您应该查看LazyTableImages示例应用程序。

答案 1 :(得分:1)

如果您已将其缩小到网络活动范围,我会尝试封装您的请求,以确保它是主线程的100%折扣。虽然您可以异步使用NSURLConnection并响应它的委托方法,但我发现在后台操作中包装同步请求更容易。如果您的需求更复杂,您可以使用NSOperation或Grand central dispatch。 imageLoader实现中的(相对)简单示例可以是:

// 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;
}

答案 2 :(得分:0)

在imageview上绘制图像时,实际上会读取磁盘中的图像。即使我们从磁盘缓存图像读取它也不会影响,因为它只是保持对文件的引用。为此,您可能必须使用较大图像的平铺。

此致 迪帕

答案 3 :(得分:0)

我遇到了这个问题 - 你在滚动时UI的加载速度有多快 - 所以我只是解决问题并改善用户体验。

  1. 仅当滚动位于新的“页面”(静态矩形)和
  2. 时才加载图像
  3. 将活动指示器置于透明滚动视图后面,以处理用户滚动速度超过应用程序可以加载内容的情况

答案 4 :(得分:0)

通常是图像解码需要花费时间,导致UI冻结(因为它全部发生在主线程上)。每次在大图像上调用[UIImage imageWithData:]时,您都会注意到打嗝。解码较小的图像要快得多。

两个选项:

  1. 您可以先加载每张图片的缩略图版本,然后在didFinishScrolling上“加强”。缩略图应该足够快地解码,以便您不会跳过任何帧。
  2. 您可以加载每个图像的缩略图版本或首先显示加载指示符,然后在另一个线程上解码全分辨率图像,并在准备就绪时将其转入。 (这是本机照片应用中使用的技术)。
  3. 我更喜欢第二种方法;现在GDC很容易。