Обработка загрузки изображения с помощью SDWebImage при повторном использовании UITableViewCell

Я использовал стороннюю библиотеку SDWebImage для загрузки изображения для своих ячеек UITableView, UIImageView создается внутри ячейки и запускает запрос при такой настройке ячейки.

[imageView setImageWithURL:[NSURL URLWithString:imageUrl] placeholderImage:[UIImage imageNamed:@"default.jpg"] completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType) {
    }];

Он работает нормально, однако, когда я быстро прокручиваю, большинство изображений загружаются не полностью (я вижу это в Чарльзе), потому что изображения не кэшируются. Как кэшировать уже отправленный запрос, даже если моя ячейка использовалась повторно, чтобы один и тот же запрос не повторялся несколько раз.

Пожалуйста, не обращайте внимания на опечатки :)


person fztest1    schedule 22.05.2014    source источник


Ответы (3)


Начиная с iOS 10 код ручной предварительной выборки моего исходного ответа больше не нужен. Просто установите prefetchDataSource. Например, в Swift 3:

override func viewDidLoad() {
    super.viewDidLoad()

    tableView.prefetchDataSource = self
}

И затем есть prefetchRowsAtIndexPaths, который использует SDWebImagePrefetcher для выборки строк

extension ViewController: UITableViewDataSourcePrefetching {
    public func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
        let urls = indexPaths.map { baseURL.appendingPathComponent(images[$0.row]) }
        SDWebImagePrefetcher.shared().prefetchURLs(urls)
    }
}

И у вас может быть стандартный cellForRowAt:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
    let url = baseURL.appendingPathComponent(images[indexPath.row])
    cell.imageView?.sd_setImage(with: url, placeholderImage: placeholder)
    return cell
}

Лично я предпочитаю AlamofireImage. Так что UITableViewDataSourcePrefetching немного отличается

extension ViewController: UITableViewDataSourcePrefetching {
    public func tableView(_ tableView: UITableView, prefetchRowsAt indexPaths: [IndexPath]) {
        let requests = indexPaths.map { URLRequest(url: baseURL.appendingPathComponent(images[$0.row])) }
        AlamofireImage.ImageDownloader.default.download(requests)
    }
}

И, очевидно, cellForRowAt будет использовать af_setImage:

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
    let url = baseURL.appendingPathComponent(images[indexPath.row])
    cell.imageView?.af_setImage(withURL: url, placeholderImage: placeholder)
    return cell
}

Мой первоначальный ответ ниже показывает для Objective-C, как вы можете сделать это в версиях iOS до 10 (где нам пришлось выполнять собственные расчеты предварительной выборки).


Такое поведение, отмена загрузки ячеек, которые больше не видны, — это именно то, что делает асинхронное извлечение изображений таким отзывчивым при быстрой прокрутке даже при медленном интернет-соединении. Например, если вы быстро прокрутите вниз до 100-й ячейки табличного представления, вы действительно не хотите, чтобы поиск этого изображения отставал от поиска изображения для предыдущих 99 строк (которые больше не видны). Я бы предложил оставить категорию UIImageView в покое, но вместо этого использовать SDWebImagePrefetcher, если вы хотите предварительно выбрать изображения для ячеек, до которых вы, вероятно, будете прокручивать.

Например, когда я вызываю reloadData, я также предварительно выбираю изображения для десяти ячеек, непосредственно предшествующих и следующих за видимыми в данный момент ячейками:

[self.tableView reloadData];
dispatch_async(dispatch_get_main_queue(), ^{
    [self prefetchImagesForTableView:self.tableView];
});

Точно так же каждый раз, когда я прекращаю прокрутку, я делаю то же самое:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    [self prefetchImagesForTableView:self.tableView];
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    if (!decelerate)
        [self prefetchImagesForTableView:self.tableView];
}

С точки зрения того, как я делаю эту предварительную выборку десяти предыдущих и следующих ячеек, я делаю это так:

#pragma mark - Prefetch cells

static NSInteger const kPrefetchRowCount = 10;

/** Prefetch a certain number of images for rows prior to and subsequent to the currently visible cells
 *
 * @param  tableView   The tableview for which we're going to prefetch images.
 */

- (void)prefetchImagesForTableView:(UITableView *)tableView {
    NSArray *indexPaths = [self.tableView indexPathsForVisibleRows];
    if ([indexPaths count] == 0) return;

    NSIndexPath *minimumIndexPath = indexPaths[0];
    NSIndexPath *maximumIndexPath = [indexPaths lastObject];

    // they should be sorted already, but if not, update min and max accordingly

    for (NSIndexPath *indexPath in indexPaths) {
        if ([minimumIndexPath compare:indexPath] == NSOrderedDescending)
            minimumIndexPath = indexPath;
        if ([maximumIndexPath compare:indexPath] == NSOrderedAscending)
            maximumIndexPath = indexPath;
    }

    // build array of imageURLs for cells to prefetch

    NSMutableArray<NSIndexPath *> *prefetchIndexPaths = [NSMutableArray array];

    NSArray<NSIndexPath *> *precedingRows = [self tableView:tableView indexPathsForPrecedingRows:kPrefetchRowCount fromIndexPath:minimumIndexPath];
    [prefetchIndexPaths addObjectsFromArray:precedingRows];

    NSArray<NSIndexPath *> *followingRows = [self tableView:tableView indexPathsForFollowingRows:kPrefetchRowCount fromIndexPath:maximumIndexPath];
    [prefetchIndexPaths addObjectsFromArray:followingRows];

    // build array of imageURLs for cells to prefetch (how you get the image URLs will vary based upon your implementation)

    NSMutableArray<NSURL *> *urls = [NSMutableArray array];
    for (NSIndexPath *indexPath in prefetchIndexPaths) {
        NSURL *url = self.objects[indexPath.row].imageURL;
        if (url) {
            [urls addObject:url];
        }
    }

    // now prefetch

    if ([urls count] > 0) {
        [[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:urls];
    }
}

/** Retrieve NSIndexPath for a certain number of rows preceding particular NSIndexPath in the table view.
 *
 * @param  tableView  The tableview for which we're going to retrieve indexPaths.
 * @param  count      The number of rows to retrieve
 * @param  indexPath  The indexPath where we're going to start (presumably the first visible indexPath)
 *
 * @return            An array of indexPaths.
 */

- (NSArray<NSIndexPath *> *)tableView:(UITableView *)tableView indexPathsForPrecedingRows:(NSInteger)count fromIndexPath:(NSIndexPath *)indexPath {
    NSMutableArray *indexPaths = [NSMutableArray array];
    NSInteger row = indexPath.row;
    NSInteger section = indexPath.section;

    for (NSInteger i = 0; i < count; i++) {
        if (row == 0) {
            if (section == 0) {
                return indexPaths;
            } else {
                section--;
                row = [tableView numberOfRowsInSection:section] - 1;
            }
        } else {
            row--;
        }
        [indexPaths addObject:[NSIndexPath indexPathForRow:row inSection:section]];
    }

    return indexPaths;
}

/** Retrieve NSIndexPath for a certain number of following particular NSIndexPath in the table view.
 *
 * @param  tableView  The tableview for which we're going to retrieve indexPaths.
 * @param  count      The number of rows to retrieve
 * @param  indexPath  The indexPath where we're going to start (presumably the last visible indexPath)
 *
 * @return            An array of indexPaths.
 */

- (NSArray<NSIndexPath *> *)tableView:(UITableView *)tableView indexPathsForFollowingRows:(NSInteger)count fromIndexPath:(NSIndexPath *)indexPath {
    NSMutableArray *indexPaths = [NSMutableArray array];
    NSInteger row = indexPath.row;
    NSInteger section = indexPath.section;
    NSInteger rowCountForSection = [tableView numberOfRowsInSection:section];

    for (NSInteger i = 0; i < count; i++) {
        row++;
        if (row == rowCountForSection) {
            row = 0;
            section++;
            if (section == [tableView numberOfSections]) {
                return indexPaths;
            }
            rowCountForSection = [tableView numberOfRowsInSection:section];
        }
        [indexPaths addObject:[NSIndexPath indexPathForRow:row inSection:section]];
    }

    return indexPaths;
}
person Rob    schedule 23.05.2014
comment
dispatch_async(dispatch_get_main_queue(), ^...{ Вы хотели сделать это в фоновом потоке? - person Alexandre G; 09.05.2015
comment
@AlexandreG - Нет, я просто хотел отложить это до тех пор, пока не произойдет перезагрузка таблицы, и, следовательно, вернуться в основную очередь. - person Rob; 09.05.2015
comment
@Роб Спасибо за ответ. Я все еще в замешательстве. Вызывается ли reloadData из потока, отличного от пользовательского интерфейса? (Я думал, что это не нормально?). Или, если он находится в потоке пользовательского интерфейса, будет ли вызов [self prefetchImagesForTableView: ] каким-то образом запускаться одновременно/до reloadData и dispatch_async в основную очередь - это способ заставить его запускаться после? - person Alexandre G; 11.05.2015
comment
Нет, reloadData происходит в основной очереди. Это старый трюк асинхронной отправки из основной очереди обратно в основную очередь: он используется, когда вы хотите добавить что-то в основную очередь, но вы хотите дать основному потоку возможность сначала сделать что-то еще (например, перезагрузить табличное представление). ). В итоге я хочу, чтобы cellForRowAtIndexPath инициировал выборку изображений для видимых строк до того, как я начну предварительную выборку изображений для ячеек, которые еще не видны. Это своего рода аналог dispatch_after или NSTimer. Это загадочный маленький трюк: извините за это. - person Rob; 11.05.2015
comment
@ Роб О нет, не надо. Я уверен, что я буду не единственным, кто будет счастлив узнать о другом способе избежать магии с номерами dispatch_after. Спасибо, что нашли время - person Alexandre G; 12.05.2015
comment
@Rob моя реализация выглядит так: у меня есть представление коллекции, и все ячейки имеют представления изображений. Каждая видимая ячейка будет отображать изображение с sd_image и индикатором загрузки. Если изображение отсутствует, индикатора загрузки нет. Теперь с этим я также хочу реализовать эту функцию предварительной выборки. Возможно ли сделать и то, и другое, поскольку оба будут отправлять запросы несколько раз, возможно, для одних и тех же ячеек. Можете ли вы предложить мне какой-то подход? - person Pooja Shah; 03.06.2015
comment
Откуда вы берете URL-адреса? Я только что преобразовал этот код в Swift и, честно говоря, понятия не имею, что происходит. Я преобразовал его в быстрый и назвал его после моих функций перезагрузки данных. Но опять же, откуда prefetchURLs получает URL-адреса?? - person GgnDpSingh; 27.12.2015
comment
@GgnDpSingh - извините за это; Я обновил это примером того, как вы берете массив NSIndexPath и строите из него массив NSURL. К сожалению, детали здесь во многом зависят от реализации вашей модели. Но суть в том, что просто создайте массив NSURL из вашего массива NSIndexPath. - person Rob; 27.12.2015
comment
Кстати, реализация SDWebImagePrefetcher сейчас безнадежно сломана. См. проблему #1366. - person Rob; 27.12.2015
comment
Я пытался обойти сломанный SDWebImagePrefetcher и немного заставил все работать. Но теперь SDWebImageRefreshCached не работает. Всегда есть что :) - person GgnDpSingh; 27.12.2015
comment
Может кто-нибудь перевести ответ на Swift, пожалуйста? - person John; 20.01.2017
comment
@John — используйте KingFisher или, если вы уже используете Alamofire, используйте AlamofireImage. - person Rob; 20.01.2017
comment
@Rob Используя AlamofireImage, изображения загружаются автоматически? SDWebImage не загружает изображения, которые появляются после прокрутки. - person JCarlosR; 08.03.2017
comment
@JCarlos - Да, вы можете использовать AlamofireImage. На самом деле я считаю его более надежным, чем SDWebImage. Я пересмотрел свой ответ, добавив несколько примеров Swift, один с AlamofireImage, а другой с SDWebImage и prefetchDataSource. - person Rob; 08.03.2017
comment
@Rob Итак, с AlamofireImage или SDWebImage нам не нужен метод cancelPrefetchingForRowsAt, я думаю - person EI Captain v2.0; 17.05.2019
comment
Можно, конечно, реализовать cancelPrefetchingForRowsAt. Это немного сложнее. Например, в AlamofireImage вы должны отслеживать RequestReceipt, чтобы впоследствии отменить его. - person Rob; 17.05.2019
comment
@Rob Есть ли какой-либо возможный эффект, чтобы не использовать метод cancelPrefetchingForRowsAt? - person EI Captain v2.0; 18.05.2019
comment
Конечно, вы можете продолжать выполнять запросы, которые, по мнению ОС, вам больше не нужны (на данный момент). Это особенно важно, если ваши запросы выполняются медленно. (Кстати, если у вас есть дополнительные вопросы, я предлагаю вам просто опубликовать свой вопрос, а не продолжать долгий обмен мнениями в комментариях.) - person Rob; 18.05.2019

Строка, из-за которой загрузка отменяется, находится в UIImageView+WebCache.m.

Первая строка в - (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletedBlock)completedBlock

звонит [self cancelCurrentImageLoad];. Если вы избавитесь от этой строки, операции загрузки должны продолжаться.

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

Есть несколько способов исправить это. Самым простым может быть просто добавить другой связанный объект в UIImageView+WebCache, просто последний загруженный URL. Затем вы можете проверить этот URL-адрес в блоке завершения и установить изображение, только если оно соответствует.

person Dima    schedule 22.05.2014
comment
Да, ты прав. Если мы сможем отслеживать последний отправленный запрос, я думаю, мы сможем справиться с этим случаем. - person fztest1; 23.05.2014
comment
Я только что отредактировал свой пост с идеей, как справиться с этим в конце. - person Dima; 23.05.2014
comment
о Звучит отличная идея. Не могли бы вы рассказать подробнее о prefetcher? Мне нужно справиться с этим самостоятельно или SDWebimgae его поддерживает? - person fztest1; 23.05.2014
comment
Отличное решение, но я до сих пор не понимаю, как можно запустить несколько загрузок для одного UIImageView, если - (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)параметры заполнителя:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock) progressBlock Completed:(SDWebImageCompletedBlock)completedBlock вызывался только один раз для одной ячейки с его UIImageView. Не могли бы вы объяснить, пожалуйста? - person Maria; 26.05.2014
comment
@Maria При повторном использовании ячейки есть возможность использовать один и тот же ImageView для нескольких запросов. - person fztest1; 28.05.2014

Я столкнулся с той же проблемой, я обнаружил, что UIImageView + WebCache отменяет последнюю загрузку, когда приходит новая загрузка.

Не уверен, что это замысел автора. Поэтому я пишу новую category базу UIImageView на SDWebImage.

Чтобы просмотреть больше: ImageDownloadGroup

Легко использовать:

[cell.imageView mq_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
                   groupIdentifier:@"customGroupID"
                         completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {

                         }];
person maquannene    schedule 21.04.2016