UIImage导致UICollectionView滚动性能差

8
我在我的应用程序中有一个UICollectionView,每个单元格都是一个UIImageView和一些文本标签。问题在于当我让UIImageView显示它们的图像时,滚动性能非常差。它的滚动体验远不及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();

再次说,我不确定如何/在哪里实现它。

非常感谢。


发布你的代码!你尝试过什么?为什么你的应用程序性能缓慢?对其进行分析以查看发生了什么,得出了什么结果? - Jim Puls
我的整个应用程序并不是性能缓慢,只有在UICollectionView中滚动单元格时才会出现这种情况。应用程序的其余部分表现非常良好。我目前正在使用cell.cardImageView.image = [UIImage imageWithData:[NSData dataWithContentsOfFile:tempCard]];来设置图像。所使用的图像不是应用程序包的一部分,而是通常下载并存储在Caches文件夹中。 - Nick
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;
}

@Nick - 希望对你有帮助,或者如果你已经解决了问题,那么对于以后遇到类似问题的人也是有帮助的。我已经修复了它,并在一些困难情况下进行了测试,包括文件URL和许多匹配的文件或Web请求。 - danh
可以通过使用collectionView:didEndDisplayingCell:forItemAtIndexPath:来改进。 - catamphetamine
在我的情况下,当尝试获取服务器上不存在的远程图像时,应用程序会崩溃。因此,在ImageRequest.m文件的完成处理程序行47中,我添加了以下条件: if (!error && [(NSHTTPURLResponse*)response statusCode] == 200) { [...]谢谢 - julio
在这行代码 if (image) return completion(image, nil); 中发生了什么?该行代码在一个 void 方法中返回了一个 block,那么这个 block 被返回到哪里了呢? - Idr
这并不是返回代码块,而是调用代码块,将承诺的图像传递给调用者(传递代码块的人)。返回部分会停止函数的执行。 - danh
显示剩余15条评论

8
一般而言,UICollectionViews或UITableViews的滚动行为不佳是因为iOS在主线程中对单元格进行了出列和构造。很难预缓存单元格或在后台线程中构造它们,相反,它们会在滚动时被出列和构建,从而阻塞UI。(尽管这样做简化了问题,因为您不必担心潜在的线程问题,但我个人认为这是苹果的糟糕设计。我认为他们应该提供一个挂钩来为UICollectionViewCell / UITableViewCell池提供自定义实现,以处理单元格的出列/重用。)
性能下降最重要的原因确实与图像数据有关(按降序排列),根据我的经验,包括:
- 同步调用下载图像数据:始终异步执行此操作,并在准备好的情况下在主线程中调用[UIImageView setImage:],使用构造的图像。 - 同步调用从本地文件系统或其他序列化数据构造图像的函数:也要异步执行此操作(例如[UIImage imageWithContentsOfFile:],[UIImage imageWithData:]等)。 - 调用[UIImage imageNamed:]函数:加载此图像的第一次服务于文件系统。你可能想要预缓存图像(只需在构造单元格之前加载[UIImage imageNamed:]即可),以便立即从内存中提供它们。 - 调用[UIImageView setImage:]也不是最快的方法,但通常无法避免,除非使用静态图像。对于静态图像,有时更快的方法是使用不同的图片视图,根据是否应显示将其设置为隐藏或不隐藏,而不是在同一图像视图上更改图像。 - 第一次出列单元格时,它要么从Nib加载,要么通过alloc-init构造,并设置了某些初始布局或属性(如果您使用了它们,则可能还包括图像)。这会导致第一次使用单元格时滚动行为不佳。
由于我非常挑剔平滑滚动(即使只是第一次使用单元格),我构建了一个整体框架来通过子类化UINib来预缓存单元格(这基本上是您得到的唯一钩子进入iOS使用的出列过程)。但这可能超出了您的需求。

1

我在使用UICollectionView时遇到了滚动问题。

对我来说,几乎完美的解决方案是:我用90x90的png缩略图填充了单元格。我说几乎完美是因为第一次完整滚动不太流畅,但再也没有崩溃了。

在我的情况下,单元格大小为90x90。

之前我有很多原始的png尺寸,当png原始尺寸大于~1000x1000时(第一次滚动时)非常卡顿。

所以,在UICollectionView上选择90x90(或类似大小),然后显示原始的png(无论大小)。希望能帮助其他人。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接