13

UICollectionView在一个显示大量照片(50-200)的应用程序中使用,但我在让它变得活泼时遇到问题(例如像照片应用程序一样活泼)。

我有一个自定义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 添加一些缓存...

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

异步加载图像......(并保持缓存)

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

这看起来很有希望,但有一个我无法追踪的错误。在初始视图加载时,所有图像都是相同的图像,并且当您滚动整个块时会加载另一个图像,但都具有该图像。我试过调试它,但我无法弄清楚。

我也试过用 NSOperation 代替,Dispatch::Queue.concurrent.async { ... }但这似乎是一样的。

如果我能让它正常工作,我认为这可能是正确的方法。

方法#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 是罪魁祸首。尽管我使用这种方法预热了 UIImages 的缓存,但 UIImage 似乎在内部做了某种惰性缓存。在更新了我的缓存预热代码以使用此处详述的解决方案 - stackoverflow.com/a/10664707/71810 - 我终于获得了超级流畅的 ~60FPS 滚动。

4

2 回答 2

7

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

@image在操作块中分配给整个集合视图类的私有变量。你可能应该改变:

@image = UIImage.imageWithContentsOfFile(image_path)

image = UIImage.imageWithContentsOfFile(image_path)

并更改所有引用@image以使用局部变量。我敢打赌,问题在于每次创建操作块并分配给实例变量时,都会替换已经存在的内容。由于操作块出列方式的一些随机性,主队列异步回调正在获取相同的图像,因为它正在访问最后一次@image分配的时间。

本质上,@image它就像操作和异步回调块的全局变量。

于 2012-12-13T21:52:30.823 回答
1

我发现解决此问题的最佳方法是保留我自己的图像缓存,并在后台线程上的 viewWillAppear 上对其进行预热,如下所示:

- (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,但我选择了 NSThread,因为一次只能运行其中一个,因此不需要队列。此外,如果您有基本上无限数量的图像,那么您将必须比我更加小心。您必须更聪明地在用户滚动位置周围加载图像,而不是像我在这里所做的那样全部加载。我可以这样做,因为对我来说,图像的数量是固定的。

我还应该补充一点,您的 collectionView:cellForItemAtIndexPath: 应该使用您的缓存,如下所示:

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

确保在收到内存警告时清除缓存,或使用类似 NSCache 的东西。我正在使用一个名为 Nimbus 的框架,它有一个名为 NIImageMemoryCache 的东西,它可以让我设置要缓存的最大像素数。很方便。

除此之外,要注意的一个问题是不要使用 UIImage 的“imageNamed”方法,它自己缓存,会给你带来内存问题。请改用“imageWithContentsOfFile”。

于 2012-12-16T11:19:07.397 回答