Правильный способ борьбы с повторным использованием ячеек с фоновыми потоками?

У меня есть UICollectionView, но те же методы должны применяться к UITableViews. Каждая из моих ячеек содержит изображение, которое я загружаю с диска, что является медленной операцией. Чтобы смягчить это, я использую асинхронную очередь отправки. Это прекрасно работает, но при быстром прокрутке результаты этих операций складываются, так что ячейка будет последовательно менять свое изображение из одного в другое, пока окончательно не остановится при последнем вызове.

Раньше я делал чек, чтобы увидеть, была ли ячейка все еще видна, и если нет, я не буду продолжать. Однако это не работает с UICollectionView, и в любом случае оно неэффективно. Я рассматриваю возможность миграции для использования NSOperations, которая может быть отменена, так что только последний вызов для изменения ячейки будет проходить. Я мог бы сделать это, проверив, завершилась ли операция в методе prepareForReuse и отменила ее, если нет. Я надеюсь, что кто-то справился с этой проблемой в прошлом и может предоставить некоторые советы или решение.

Ответ 1

Сессия 211 - Создание параллельных пользовательских интерфейсов на iOS, начиная с WWDC 2012, обсуждает проблему изменения изображения ячейки, поскольку фоновые задачи догоняют (начиная с 38 м15 с).

Вот как вы решаете эту проблему. После того, как вы загрузили изображение в фоновом режиме, используйте путь индекса, чтобы найти текущую ячейку, если она есть, которая отображает элемент, содержащий это изображение. Если вы получите ячейку, установите изображение ячейки. Если вы получаете нуль, в данный момент ячейка не отображает этот элемент, поэтому просто удаляйте изображение.

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

Ответ 2

Вы пытались использовать SDWebImage среду с открытым исходным кодом, которая работает с загрузкой изображений из Интернета и кешированием их асинхронно? Он отлично работает с UITableViews и должен быть таким же хорошим с UICollectionViews. Вы просто используете метод setImage:withURL: расширения UIImageView, и он делает магию.

Ответ 3

У меня была та же проблема с UITableView около 400 элементов и найти жестокое, но рабочее решение: не используйте повторно ячейки! Зависит от того, насколько и насколько велики изображения, но на самом деле iOS достаточно эффективен при освобождении неиспользуемых ячеек... Опять же: не то, что предлагает пурист, но пара с асинхронной нагрузкой быстро освещает, не требует ошибок и не требует сложной логики синхронизации.