使用后台线程处理单元重用的正确方法?

时间:2012-12-17 20:48:36

标签: objective-c multithreading ios6 nsoperation uicollectionview

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

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

3 个答案:

答案 0 :(得分:21)

Session 211 - Building Concurrent User Interfaces on iOS, from WWDC 2012,讨论了当背景任务赶上时(从38m15开始)细胞图像变化的问题。

以下是您解决该问题的方法。将图像加载到后台队列后,使用索引路径查找显示包含该图像的项目的当前单元格(如果有)。如果您有一个单元格,请设置单元格的图像。如果你得到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];
}

答案 1 :(得分:8)

您是否尝试过使用SDWebImage开源框架,该框架可以从网上下载图像并以异步方式缓存它们?它对UITableViews非常有效,并且与UICollectionViews一样好。您只需使用setImage:withURL:扩展程序的UIImageView方法,就可以实现神奇。

答案 2 :(得分:-2)

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