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

102
16

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


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

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


Пожалуйста, игнорируйте любую опечатку:)

спросил(а) 2014-05-23T01:23:00+04:00 6 лет, 5 месяцев назад
1
Решение
120

Эффективный 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;
}

ответил(а) 2014-05-23T08:17:00+04:00 6 лет, 5 месяцев назад
71

Линия, вызывающая отмену загрузки, находится в 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-адрес в блоке завершения и установить только изображение, если оно соответствует.

ответил(а) 2014-05-23T01:39:00+04:00 6 лет, 5 месяцев назад
41

Я встретил ту же проблему, я обнаружил, что 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) {

}];

ответил(а) 2016-04-21T04:39:00+03:00 4 года, 6 месяцев назад
Ваш ответ
Введите минимум 50 символов
Чтобы , пожалуйста,
Выберите тему жалобы:

Другая проблема