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

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

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


person akaru    schedule 17.12.2012    source источник


Ответы (3)


Сессия 211 — Создание параллельных пользовательских интерфейсов на 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];
}
person rob mayoff    schedule 17.12.2012
comment
Собираюсь проверить эту сессию - не знаю, почему я еще этого не сделал. Попробую ваш ответ - спасибо за такую ​​​​подробность. - person akaru; 18.12.2012
comment
Сближаемся с этим. Почти работает, но имеет тенденцию загружать одну и ту же ячейку несколько раз, не загружая другие. Я думаю, что это может быть связано с тем, что [indexPathsNeedingImages anyObject] каждый раз возвращает один и тот же путь индекса. - person akaru; 18.12.2012
comment
Если вы используете глобальную очередь, она не является последовательной. - person rob mayoff; 18.12.2012
comment
Я определенно использую последовательную очередь (dispatch_queue_create(...)) - person akaru; 18.12.2012
comment
Я понял проблему. Ознакомьтесь с исправленным bg_loadOneImage. Там необходимо удалить индексный путь из набора, так как bg_loadImageForRowAtIndexPath использует dispatch_async для установки изображения в основной поток. Другой bg_loadOneImage может начать выполняться в фоновой очереди до того, как основная очередь удалит индексный путь из набора. - person rob mayoff; 18.12.2012
comment
Вы также хотите удалить его в setImage:forItemAtIndexPath: (как я сделал выше), потому что путь индекса мог быть повторно добавлен в набор, пока фоновая задача загружала свое изображение. - person rob mayoff; 18.12.2012
comment
Это имеет смысл. Спасибо, что поняли это. Отличный ответ. - person akaru; 18.12.2012
comment
На устройстве это все еще вызывает некоторые странные эффекты в отдельных ячейках, но это определенно привело меня в правильное состояние. Собираюсь создать решение на основе этого. - person akaru; 18.12.2012
comment
Я написал пост на основе сеанса Apple 211 — stavash.wordpress.com/2012/12/14/ - person Stavash; 03.12.2013
comment
В cellForItemAtIndexPath должно быть bg_loadImageForRowAtIndexPath, а не bg_loadImageForItemAtIndexPath - person barfoon; 19.08.2014

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

person Eugene    schedule 17.12.2012
comment
+1 для SDWebImage. Он очень эффективен в представлениях коллекций и представлениях таблиц. - person MishieMoo; 18.12.2012
comment
Мои изображения получены локально. - person akaru; 18.12.2012
comment
Хотя SDWebImage великолепен, это не решение основной проблемы. Это все еще асинхронно. Очень быстрая загрузка изображений из кеша на самом деле просто скрывает проблему. Если ячейка повторно используется до загрузки изображения, изображение будет загружено в неправильную ячейку. Так что магия в данном случае — это злая магия. - person Joel; 20.12.2015

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

person il Malvagio Dottor Prosciutto    schedule 17.12.2012
comment
Даже если люди не согласны, это жестокое, но полезное решение... ;) - person il Malvagio Dottor Prosciutto; 25.08.2015