如何获得有很多图像(50-200)snappy UICollectionView?

我在一个显示相当多的照片(50-200)的应用程序中使用UICollectionView ,我遇到的问题变得活泼(像照片应用程序一样快)。

我有一个自定义UICollectionViewCellUIImageView作为它的子视图。 我将UIImage.imageWithContentsOfFile:从文件系统加载到单元格内的UIImageViews中。

现在我已经尝试了很多方法,但是它们一直是越野车或性能问题。

注意:我使用RubyMotion,所以我会用Ruby风格编写任何代码片段。

首先,这里是我的自定义UICollectionViewCell类供参考…

 class CustomCell < UICollectionViewCell def initWithFrame(rect) super @image_view = UIImageView.alloc.initWithFrame(self.bounds).tap do |iv| iv.contentMode = UIViewContentModeScaleAspectFill iv.clipsToBounds = true iv.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth self.contentView.addSubview(iv) end self end def prepareForReuse super @image_view.image = nil end def image=(image) @image_view.image = image end end 

方法#1

保持简单

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

使用此滚动向上/向下是可怕的。 这是跳动和缓慢。

方法#2

通过NSCache添加一些caching…

 def viewDidLoad ... @images_cache = NSCache.alloc.init ... 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 image = UIImage.imageWithContentsOfFile(image_path) @images_cache.setObject(image, forKey: image_path) cell.image = image end end 

再一次,在第一次完整的滚动,跳动和缓慢,但从那时起,它是平稳的航行。

方法#3

加载图像asynchronous…(并保持caching)

 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 

这看起来很有希望,但有一个我不能追查的错误。 在初始视图加载所有的图像是相同的图像和滚动整个块与另一个图像加载,但都有该图像。 我试过debugging,但我无法弄清楚。

我也尝试用Dispatch::Queue.concurrent.async { ... }replaceNSOperation,但似乎是相同的。

我想这可能是正确的方法,如果我能得到它的正常工作。

方法#4

在挫折中,我决定只是预先加载所有的图像作为UIImages …

 def viewDidLoad ... @preloaded_images = @image_paths.map do |path| UIImage.imageWithContentsOfFile(path) end ... end def collectionView(collection_view, cellForItemAtIndexPath: index_path) cell = collection_view.dequeueReusableCellWithReuseIdentifier(CELL_IDENTIFIER, forIndexPath: index_path) cell.image = @preloaded_images[index_path.row] end 

这在第一个完整的卷轴中变得有点缓慢和跳动,但之后都是好的。

概要

所以,如果有人能指出我的方向,我会非常感激。 照片应用程序如何做得这么好


其他人的注意:[接受]的答案解决了我的问题出现在错误的单元格的图像,但即使调整我的图像大小正确后,我仍然不断滚动滚动。 在剥离我的代码之后,我终于发现UIImage#imageWithContentsOfFile是罪魁祸首。 即使我用这种方法预热了UIImage的caching,UIImage似乎在内部做了一些懒惰的caching。 更新我的caching预热代码使用这里详细的解决scheme – stackoverflow.com/a/10664707/71810 – 我终于有了超级平滑〜60FPS滚动。

我认为方法3是最好的方法,我想我可能已经发现了这个错误。

您将在操作块中分配给整个集合视图类的@image (私有variables)。 你应该改变:

 @image = UIImage.imageWithContentsOfFile(image_path) 

 image = UIImage.imageWithContentsOfFile(image_path) 

并更改@image所有引用以使用局部variables。 我敢打赌,问题是每次创build一个操作块并分配给实例variables时,都会replace已经存在的内容。 由于操作块如何出队的一些随机性,主队列asynchronouscallback将获取相同的图像,因为它正在访问最后一次分配@image时间。

实质上, @image就像是一个全局variables,用于操作和asynchronouscallback块。

我发现解决这个问题的最好方法是保留我自己的图像caching,并在viewWill上预热它在后台线程上,如下所示:

 - (void) warmThubnailCache { if (self.isWarmingThumbCache) { return; } if ([NSThread isMainThread]) { // do it on a background thread [NSThread detachNewThreadSelector:@selector(warmThubnailCache) toTarget:self withObject:nil]; } else { self.isWarmingThumbCache = YES; NIImageMemoryCache *thumbCache = self.thumbnailImageCache; for (GalleryImage *galleryImage in _galleryImages) { NSString *cacheKey = galleryImage.thumbImageName; UIImage *thumbnail = [thumbCache objectWithName:cacheKey]; if (thumbnail == nil) { // populate cache thumbnail = [UIImage imageWithContentsOfFile:galleryImage.thumbImageName]; [thumbCache storeObject:thumbnail withName:cacheKey]; } } self.isWarmingThumbCache = NO; } } 

你也可以使用GCD而不是NSThread,但是我select了NSThread,因为一次只能运行其中的一个,所以不需要队列。 另外,如果您拥有的图像数量基本上是无限的,那么您将必须比I更加谨慎。您必须更加聪明地将图像加载到用户的滚动位置周围,而不是像所有这些位置一样。 我可以这样做,因为对于我来说,图像的数量是固定的。

我还应该添加你的collectionView:cellForItemAtIndexPath:应该使用你的caching,就像这样:

 - (PSUICollectionViewCell *)collectionView:(PSUICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"GalleryCell"; GalleryCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier forIndexPath:indexPath]; GalleryImage *galleryImage = [_galleryManager.galleryImages objectAtIndex:indexPath.row]; // use cached images first NIImageMemoryCache *thumbCache = self.galleryManager.thumbnailImageCache; NSString *cacheKey = galleryImage.thumbImageName; UIImage *thumbnail = [thumbCache objectWithName:cacheKey]; if (thumbnail != nil) { cell.imageView.image = thumbnail; } else { UIImage *image = [UIImage imageWithContentsOfFile:galleryImage.thumbImageName]; cell.imageView.image = image; // store image in cache [thumbCache storeObject:image withName:cacheKey]; } return cell; } 

确保你的caching清除,当你得到一个内存的警告,或使用像NSCache的东西。 我使用的是一个名为Nimbus的框架,它有一些名为NIImageMemoryCache,它可以让我设置最大数量的像素caching。 非常方便。

除此之外,需要注意的一点是不要使用UIImage的“imageNamed”方法,它自己的caching,并会给你内存的问题。 改用“imageWithContentsOfFile”。