8

我的应用程序中有一个 UICollectionView,每个单元格都是一个 UIImageView 和一些文本标签。问题是当我让 UIImageViews 显示他们的图像时,滚动性能很糟糕。它远不如 UITableView 的滚动体验,甚至没有 UIImageView 的相同 UICollectionView 的滚动体验那么流畅。

我从几个月前发现了这个问题,似乎找到了答案,但它是用 RubyMotion 编写的,我不明白。我试图看看如何将它转换为 Xcode,但由于我也从未使用过 NSCache,所以有点难。那里的张贴者还在这里指出除了他们的解决方案之外还要实现一些东西,但我也不知道该把代码放在哪里。可能是因为我不理解第一个问题的代码。

有人可以帮助将其翻译成 Xcode 吗?

def viewDidLoad
  ...
  @images_cache = NSCache.alloc.init
  @image_loading_queue = NSOperationQueue.alloc.init
  @image_loading_queue.maxConcurrentOperationCount = 3
  ...
end

def collectionView(collection_view, cellForItemAtIndexPath: index_path)
  cell = collection_view.dequeueReusableCellWithReuseIdentifier(CELL_IDENTIFIER, forIndexPath: index_path)
  image_path = @image_paths[index_path.row]

  if cached_image = @images_cache.objectForKey(image_path)
    cell.image = cached_image
  else
    @operation = NSBlockOperation.blockOperationWithBlock lambda {
      @image = UIImage.imageWithContentsOfFile(image_path)
      Dispatch::Queue.main.async do
        return unless collectionView.indexPathsForVisibleItems.containsObject(index_path)
        @images_cache.setObject(@image, forKey: image_path)
        cell = collectionView.cellForItemAtIndexPath(index_path)
        cell.image = @image
      end
    }
    @image_loading_queue.addOperation(@operation)
  end
end

这是第一个问题的提问者所说的第二个问题的代码解决这个问题:

UIImage *productImage = [[UIImage alloc] initWithContentsOfFile:path];

CGSize imageSize = productImage.size;
UIGraphicsBeginImageContext(imageSize);
[productImage drawInRect:CGRectMake(0, 0, imageSize.width, imageSize.height)];
productImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

同样,我不确定如何/在哪里实现它。

非常感谢。

4

3 回答 3

19

这是我遵循的模式。始终加载异步并缓存结果。当异步加载完成时,不要假设视图的状态。我有一个简化负载的类,如下所示:

//
//  ImageRequest.h

// This class keeps track of in-flight instances, creating only one NSURLConnection for
// multiple matching requests (requests with matching URLs).  It also uses NSCache to cache
// retrieved images.  Set the cache count limit with the macro in this file.

#define kIMAGE_REQUEST_CACHE_LIMIT  100
typedef void (^CompletionBlock) (UIImage *, NSError *);

@interface ImageRequest : NSMutableURLRequest

- (UIImage *)cachedResult;
- (void)startWithCompletion:(CompletionBlock)completion;

@end

//
//  ImageRequest.m

#import "ImageRequest.h"

NSMutableDictionary *_inflight;
NSCache *_imageCache;

@implementation ImageRequest

- (NSMutableDictionary *)inflight {

    if (!_inflight) {
        _inflight = [NSMutableDictionary dictionary];
    }
    return _inflight;
}

- (NSCache *)imageCache {

    if (!_imageCache) {
        _imageCache = [[NSCache alloc] init];
        _imageCache.countLimit = kIMAGE_REQUEST_CACHE_LIMIT;
    }
    return _imageCache;
}

- (UIImage *)cachedResult {

    return [self.imageCache objectForKey:self];
}

- (void)startWithCompletion:(CompletionBlock)completion {

    UIImage *image = [self cachedResult];
    if (image) return completion(image, nil);

    NSMutableArray *inflightCompletionBlocks = [self.inflight objectForKey:self];
    if (inflightCompletionBlocks) {
        // a matching request is in flight, keep the completion block to run when we're finished
        [inflightCompletionBlocks addObject:completion];
    } else {
        [self.inflight setObject:[NSMutableArray arrayWithObject:completion] forKey:self];

        [NSURLConnection sendAsynchronousRequest:self queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) {
            if (!error) {
                // build an image, cache the result and run completion blocks for this request
                UIImage *image = [UIImage imageWithData:data];
                [self.imageCache setObject:image forKey:self];

                id value = [self.inflight objectForKey:self];
                [self.inflight removeObjectForKey:self];

                for (CompletionBlock block in (NSMutableArray *)value) {
                    block(image, nil);
                }
            } else {
                [self.inflight removeObjectForKey:self];
                completion(nil, error);
            }
        }];
    }
}

@end

现在单元格(集合或表)更新相当简单:

-(UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {

    UICollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];

    NSURL *url = [NSURL URLWithString:@"http:// some url from your model"];
    // note that this can be a web url or file url

    ImageRequest *request = [[ImageRequest alloc] initWithURL:url];

    UIImage *image = [request cachedResult];
    if (image) {
        UIImageView *imageView = (UIImageView *)[cell viewWithTag:127];
        imageView.image = image;
    } else {
        [request startWithCompletion:^(UIImage *image, NSError *error) {
            if (image && [[collectionView indexPathsForVisibleItems] containsObject:indexPath]) {
                [collectionView reloadItemsAtIndexPaths:@[indexPath]];
            }
        }];
    }
    return cell;
}
于 2013-04-03T23:02:53.490 回答
8

通常,UICollectionViews 或 UITableViews 的不良滚动行为会发生,因为单元格在 iOS 的主线程中出列和构造。预缓存单元格或在后台线程中构建它们几乎没有自由,而是在您滚动阻塞 UI 时将它们出列并构建。(就我个人而言,我发现 Apple 的这种糟糕设计虽然确实简化了问题,因为您不必了解潜在的线程问题。我认为他们应该提供一个挂钩来为 UICollectionViewCell/UITableViewCell 池提供自定义实现可以处理单元的出队/重用。)

性能下降的最重要原因确实与图像数据有关,并且(按数量级递减)根据我的经验:

  • 同步调用以下载图像数据:始终异步执行此操作,并在主线程中准备好构建的图像时调用 [UIImageView setImage:]
  • 同步调用以从本地文件系统上的数据或其他序列化数据构建图像:也可以异步执行此操作。(例如 [UIImage imageWithContentsOfFile:]、[UIImage imageWithData:] 等)。
  • 调用 [UIImage imageNamed:]:第一次加载此图像时,它是从文件系统提供的。您可能想要预缓存图像(只需在实际构造单元之前加载 [UIImage imageNamed:] 以便可以立即从内存中提供它们。
  • 调用 [UIImageView setImage:] 也不是最快的方法,但通常无法避免,除非您使用静态图像。对于静态图像,有时使用您设置为隐藏或不隐藏的不同图像视图会更快,具体取决于它们是否应该显示而不是更改同一图像视图上的图像。
  • 第一次出列单元格时,它要么从 Nib 加载,要么使用 alloc-init 构造,并设置一些初始布局或属性(如果您使用它们,可能还有图像)。这会在第一次使用单元格时导致不良的滚动行为。

因为我对平滑滚动非常挑剔(即使这只是第一次使用单元格),所以我构建了一个完整的框架来通过子类化 UINib 来预缓存单元格(这基本上是您进入 iOS 使用的出队过程的唯一挂钩)。但这可能超出了您的需求。

于 2015-01-29T12:42:17.077 回答
1

我有关于UICollectionView滚动的问题。

什么对我来说(几乎)像一个魅力:我用 90x90 的 png 缩略图填充了单元格。我说几乎是因为第一个完整的滚动不是那么顺利,但再也没有崩溃过。

在我的例子中,单元格大小是 90x90。

我之前有很多原始 png 大小,当 png 原始大小大于 ~1000x1000 时,它非常不稳定(第一次滚动时很多崩溃)。

因此,我选择 90x90(或类似)UICollectionView并显示原始 png(无论大小)。希望它可以帮助别人。

于 2013-12-21T00:33:43.283 回答