10

我有一个UICollectionView,但同样的方法应该适用于UITableViews. 我的每个单元格都包含我从磁盘加载的图像,这是一个缓慢的操作。为了缓解这种情况,我使用了异步调度队列。这很好用,但是快速滚动会导致这些操作堆叠起来,这样单元格就会按顺序将其图像从一个更改为另一个,直到最终在最后一次调用时停止。

过去,我检查过该单元格是否仍然可见,如果不可见,则不继续。但是,这不适用于 UICollectionView,而且无论如何它效率不高。我正在考虑迁移到 use NSOperations,它可以被取消,这样只有最后一次修改单元格的调用才会通过。我可以通过检查prepareForReuse方法中的操作是否完成来做到这一点,如果没有完成,则取消它。我希望过去有人处理过这个问题,并可以提供一些提示或解决方案。

4

3 回答 3

21

Session 211 - Building Concurrent User Interfaces on iOS,来自 WWDC 2012,讨论了随着后台任务赶上(从 38 分 15 秒开始)单元格图像发生变化的问题。

这是您解决该问题的方法。在后台队列中加载图像后,使用索引路径查找当前单元格(如果有),它正在显示包含该图像的项目。如果你得到一个单元格,设置单元格的图像。如果您得到 nil,则当前没有单元格显示该项目,因此只需丢弃图像。

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    MyCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
    cell.imageView.image = nil;
    dispatch_async(myQueue, ^{
        [self bg_loadImageForRowAtIndexPath:indexPath];
    });
    return cell;
}

- (void)bg_loadImageForRowAtIndexPath:(NSIndexPath *)indexPath {
    // I assume you have a method that returns the path of the image file.  That
    // method must be safe to run on the background queue.
    NSString *path = [self bg_pathOfImageForItemAtIndexPath:indexPath];
    UIImage *image = [UIImage imageWithContentsOfFile:path];
    // Make sure the image actually loads its data now.
    [image CGImage];

    dispatch_async(dispatch_get_main_queue(), ^{
        [self setImage:image forItemAtIndexPath:indexPath];
    });
}

- (void)setImage:(UIImage *)image forItemAtIndexPath:(NSIndexPath *)indexPath {
    MyCell *cell = (MyCell *)[collectionView cellForItemAtIndexPath:indexPath];
    // If cell is nil, the following line has no effect.
    cell.imageView.image = image;
}

这是一种减少后台任务加载视图不再需要的图像的简单方法。给自己一个NSMutableSet实例变量:

@implementation MyViewController {
    dispatch_queue_t myQueue;
    NSMutableSet *indexPathsNeedingImages;
}

当您需要加载图像时,将单元格的索引路径放入集合中并调度一个任务,从集合中取出一个索引路径并加载其图像:

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
    MyCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];
    cell.imageView.image  = nil;
    [self addIndexPathToImageLoadingQueue:indexPath];
    return cell;
}

- (void)addIndexPathToImageLoadingQueue:(NSIndexPath *)indexPath {
    if (!indexPathsNeedingImages) {
        indexPathsNeedingImages = [NSMutableSet set];
    }
    [indexPathsNeedingImages addObject:indexPath];
    dispatch_async(myQueue, ^{ [self bg_loadOneImage]; });
}

如果单元格停止显示,请从集合中删除单元格的索引路径:

- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath {
    [indexPathsNeedingImages removeObject:indexPath];
}

要加载图像,请从集合中获取索引路径。我们必须为此调度回主队列,以避免集合上的竞争条件:

- (void)bg_loadOneImage {
    __block NSIndexPath *indexPath;
    dispatch_sync(dispatch_get_main_queue(), ^{
        indexPath = [indexPathsNeedingImages anyObject];
        if (indexPath) {
            [indexPathsNeedingImages removeObject:indexPath];
        }
    }

    if (indexPath) {
        [self bg_loadImageForRowAtIndexPath:indexPath];
    }
}

bg_loadImageForRowAtIndexPath:方法同上。但是当我们实际设置单元格中的图像时,我们还想从集合中删除单元格的索引路径:

- (void)setImage:(UIImage *)image forItemAtIndexPath:(NSIndexPath *)indexPath {
    MyCell *cell = (MyCell *)[collectionView cellForItemAtIndexPath:indexPath];
    // If cell is nil, the following line has no effect.
    cell.imageView.image = image;

    [indexPathsNeedingImages removeObject:indexPath];
}
于 2012-12-17T21:41:41.257 回答
8

您是否尝试过使用SDWebImage开源框架来处理从 Web 下载图像并异步缓存它们?它非常适合UITableViews并且应该与UICollectionViews. 您只需使用扩展的setImage:withURL:方法,UIImageView它就会变魔术。

于 2012-12-17T20:58:39.020 回答
-1

我在大约 400 个元素的 UITableView 上遇到了同样的问题,并找到了一个残酷但有效的解决方案:不要重复使用单元格!取决于图像的大小和大小,但实际上 iOS 在释放未使用的单元格方面非常有效......再次:这不是纯粹主义者所建议的,但与异步加载相结合可以快速点亮,无错误并且不必处理复杂的同步逻辑。

于 2012-12-17T20:58:59.890 回答