SDWebImage не загружает удаленные изображения до прокрутки

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

[cell.imageView setImageWithURL:url placeholderImage:[UIImage imageNamed:@"loading.jpg"]];

в cellForRowAtIndexPath: теперь проблема в том, что он загружает изображения только в видимые ячейки, а не в ячейки, которые находятся за пределами экрана, для которых мне нужно прокручивать вверх и вниз, чтобы они загружались. Есть ли способ загрузить все изображения без прокрутки таблицы. Заранее спасибо!!


person user2082760    schedule 29.03.2013    source источник
comment
Вы уверены, что создаются те ячейки, которые находятся за кадром? Вы знаете о поведении UITableView?   -  person Exploring    schedule 29.03.2013
comment
Ячейки используются повторно   -  person user2082760    schedule 29.03.2013
comment
Да, тогда какие ячейки находятся за пределами экрана, тогда для этих ячеек будет вызываться метод экземпляра? Это не проблема SDWebImage.   -  person Exploring    schedule 29.03.2013
comment
Так это нормальное поведение, когда изображения загружаются только для тех ячеек, которые видны/на экране?   -  person user2082760    schedule 29.03.2013
comment
Да это так. iOS спроектирована таким образом, чтобы уменьшить использование памяти.   -  person Exploring    schedule 29.03.2013
comment
Эта концепция загрузки изображений только для видимых ячеек и загрузки внеэкранных изображений при прокрутке называется ленивой загрузкой и является всей целью класса (а также для асинхронного выполнения). Насколько я понимаю, вы хотите объединить эту асинхронную операцию с более быстрой загрузкой, то есть начать предварительную загрузку некоторых других изображений?   -  person Rob    schedule 29.03.2013
comment
да... на самом деле я хочу предварительно загрузить изображения для ячеек за кадром, чтобы не было похоже, что загрузка изображения занимает время   -  person user2082760    schedule 29.03.2013
comment
@user2082760 user2082760 Классы SDWebImage имеют все интерфейсы для прямого взаимодействия с SDWebImageManager напрямую. Вы можете реализовать свою собственную процедуру предварительной выборки, но будьте осторожны при обработке didReceiveMemoryWarning. Возможно, вы также захотите тщательно управлять своим кешем, возможно, только с предварительной выборкой некоторого ограниченного числа предстоящих ячеек. Не многие из этих классов имеют встроенные процедуры предварительной выборки, поскольку это противоположно тому, что пытается сделать большинство людей. Но умная предварительная выборка — хорошая идея. Не слишком сложно, но и не тривиально. У вас есть хорошая модель, поддерживающая ваше табличное представление?   -  person Rob    schedule 29.03.2013
comment
я даже пробовал вариант SDWebImageProgressiveDownload, но приложение вылетает, когда я прокручиваю таблицу и без журнала сбоев.. я думаю, что это также проблема с нехваткой памяти   -  person user2082760    schedule 29.03.2013
comment
Нет, это решает другую проблему, мне кажется. Я собираюсь на звонок, но если кто-то не предоставил вам хорошую процедуру предварительной выборки к тому времени, как я вернусь через несколько часов, я вам помогу.   -  person Rob    schedule 29.03.2013


Ответы (4)


Если вы хотите выполнить предварительную выборку строк, вы можете ответить на UIScrollViewDelegate методы, чтобы определить, когда прокрутка таблицы завершена, инициируя предварительную выборку строк. Вы можете выполнить предварительную выборку, используя SDWebImagePrefetcher (в моем первоначальном ответе я немного пренебрегал этим полезным классом, но теперь он работает относительно хорошо):

- (void)viewDidLoad
{
    [super viewDidLoad];

    // the details don't really matter here, but the idea is to fetch data, 
    // call `reloadData`, and then prefetch the other images

    NSURL *url = [NSURL URLWithString:kUrlWithJSONData];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    [NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
        if (connectionError) {
            NSLog(@"sendAsynchronousRequest error: %@", connectionError);
            return;
        }

        self.objects = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];

        [self.tableView reloadData];

        [self prefetchImagesForTableView:self.tableView];
    }];
}

// some of the basic `UITableViewDataDelegate` methods have been omitted because they're not really relevant

Вот простой cellForRowAtIndexPath (не совсем уместный, но просто показывающий, что если вы используете SDWebImagePrefetcher, вам не нужно возиться с cellForRowAtIndexPath:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *cellIdentifier = @"Cell";
    CustomCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    NSAssert([cell isKindOfClass:[CustomCell class]], @"cell should be CustomCell");

    [cell.customImageView setImageWithURL:[self urlForIndexPath:indexPath] placeholderImage:nil];
    [cell.customLabel setText:[self textForIndexPath:indexPath]];

    return cell;
}

Эти методы UIScrollViewDelegate выполняют предварительную выборку большего количества строк после завершения прокрутки.

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    // if `decelerate` was true for `scrollViewDidEndDragging:willDecelerate:`
    // this will be called when the deceleration is done

    [self prefetchImagesForTableView:self.tableView];
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
{
    // if `decelerate` is true, then we shouldn't start prefetching yet, because
    // `cellForRowAtIndexPath` will be hard at work returning cells for the currently visible
    // cells.

    if (!decelerate)
        [self prefetchImagesForTableView:self.tableView];
}

Вам, очевидно, необходимо реализовать процедуру предварительной выборки. Это получает значения NSIndexPath для ячеек с каждой стороны видимых ячеек, получает их URL-адреса изображений, а затем выполняет предварительную выборку этих данных.

/** 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 (indexPath.section < minimumIndexPath.section || (indexPath.section == minimumIndexPath.section && indexPath.row < minimumIndexPath.row)) minimumIndexPath = indexPath;
        if (indexPath.section > maximumIndexPath.section || (indexPath.section == maximumIndexPath.section && indexPath.row > maximumIndexPath.row)) maximumIndexPath = indexPath;
    }

    // build array of imageURLs for cells to prefetch

    NSMutableArray *imageURLs = [NSMutableArray array];
    indexPaths = [self tableView:tableView priorIndexPathCount:kPrefetchRowCount fromIndexPath:minimumIndexPath];
    for (NSIndexPath *indexPath in indexPaths)
        [imageURLs addObject:[self urlForIndexPath:indexPath]];
    indexPaths = [self tableView:tableView nextIndexPathCount:kPrefetchRowCount fromIndexPath:maximumIndexPath];
    for (NSIndexPath *indexPath in indexPaths)
        [imageURLs addObject:[self urlForIndexPath:indexPath]];

    // now prefetch

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

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

/** 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 *)tableView:(UITableView *)tableView priorIndexPathCount:(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 *)tableView:(UITableView *)tableView nextIndexPathCount:(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;
}

Там много всего, но на самом деле SDWebImage и его SDWebImagePrefetcher делают тяжелую работу.

Я включаю свой оригинальный ответ ниже для полноты картины.


Оригинальный ответ:

Если вы хотите выполнить предварительную выборку с помощью SDWebImage, вы можете сделать что-то вроде следующего:

  1. Добавьте блок завершения к вызову setImageWithURL:

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
        NSLog(@"%s", __FUNCTION__);
    
        static NSString *cellIdentifier = @"Cell";
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    
        TableModelRow *rowData = self.objects[indexPath.row];
    
        cell.textLabel.text = rowData.title;
        [cell.imageView setImageWithURL:rowData.url
                       placeholderImage:[UIImage imageNamed:@"placeholder.png"]
                              completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType) {
                                  [self prefetchImagesForTableView:tableView];
                              }];
    
        return cell;
    }
    

    Должен признаться, мне не очень нравится вызывать мою подпрограмму prefetcher здесь (хотелось бы, чтобы в iOS был хороший метод делегата didFinishTableRefresh), но он работает, даже если он вызывает подпрограмму больше раз, чем мне действительно хотелось бы. Ниже я просто удостоверяюсь, что приведенная ниже процедура гарантирует, что она не будет делать избыточных запросов.

  2. Во всяком случае, я пишу процедуру предварительной выборки, которая ищет, скажем, следующие десять изображений:

    const NSInteger kPrefetchRowCount = 10;
    
    - (void)prefetchImagesForTableView:(UITableView *)tableView
    {
        // determine the minimum and maximum visible rows
    
        NSArray *indexPathsForVisibleRows = [tableView indexPathsForVisibleRows];
        NSInteger minimumVisibleRow = [indexPathsForVisibleRows[0] row];
        NSInteger maximumVisibleRow = [indexPathsForVisibleRows[0] row];
    
        for (NSIndexPath *indexPath in indexPathsForVisibleRows)
        {
            if (indexPath.row < minimumVisibleRow) minimumVisibleRow = indexPath.row;
            if (indexPath.row > maximumVisibleRow) maximumVisibleRow = indexPath.row;
        }
    
        // now iterate through our model;
        // `self.objects` is an array of `TableModelRow` objects, one object
        // for every row of the table.
    
        [self.objects enumerateObjectsUsingBlock:^(TableModelRow *obj, NSUInteger idx, BOOL *stop) {
            NSAssert([obj isKindOfClass:[TableModelRow class]], @"Expected TableModelRow object");
    
            // if the index is within `kPrefetchRowCount` rows of our visible rows, let's
            // fetch the image, if it hasn't already done so.
    
            if ((idx < minimumVisibleRow && idx >= (minimumVisibleRow - kPrefetchRowCount)) ||
                (idx > maximumVisibleRow && idx <= (maximumVisibleRow + kPrefetchRowCount)))
            {
                // my model object has method for initiating a download if needed
    
                [obj downloadImageIfNeeded];
            }
        }];
    }
    
  3. В процедуре загрузки вы можете проверить, началась ли загрузка образа, и если нет, то запустить ее. Чтобы сделать это с помощью SDWebImage, я храню указатель weak на операцию веб-изображения в своем классе TableModelRow (класс модели, поддерживающий отдельные строки моей таблицы):

    @property (nonatomic, weak) id<SDWebImageOperation> webImageOperation;
    

    Затем я заставляю процедуру downloadImageIfNeeded запускать загрузку, если она еще не началась (вы можете понять, почему создание этого weak было так важно... Я проверяю, есть ли в этой строке уже ожидающая операция, прежде чем запускать другую). Я ничего не делаю с загруженным изображением (за исключением, в целях отладки, регистрации факта загрузки), а просто загружаю и позволяю SDImageWeb отслеживать кэшированное изображение для меня, поэтому, когда cellForRowAtIndexPath позже запрашивает изображение, когда пользователь прокручивает вниз, оно там, готово и ждет.

    - (void)downloadImageIfNeeded
    {
        if (self.webImageOperation)
            return;
    
        SDWebImageManager *imageManager = [SDWebImageManager sharedManager];
    
        self.webImageOperation = [imageManager downloadWithURL:self.url
                                                       options:0
                                                      progress:nil
                                                     completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished) {
                                                         NSLog(@"%s: downloaded %@", __FUNCTION__, self.title);
                                                         // I'm not going to do anything with the image, but `SDWebImage` has now cached it for me
                                                     }];
    }
    

    Часть меня думает, что было бы более надежным сначала вызвать метод экземпляра imageManager.imageCache queryDiskCacheForKey, но после некоторого тестирования оказалось, что это не нужно (и downloadWithURL в любом случае делает это за нас).

Я должен отметить, что в библиотеке SDImageWeb есть класс SDWebImagePrefetcher (см. документацию). Название класса невероятно многообещающее, но, глядя на код, при всем уважении к отличной в остальном библиотеке, он не кажется мне очень надежным (например, это простой список URL-адресов для извлечения, и если вы сделаете это снова , он отменяет предыдущий список без понятия «добавление в очередь» или что-то в этом роде). Это многообещающая идея, но немного слабая в исполнении. И когда я попробовал это, мой UX заметно пострадал.

Итак, я склонен не использовать SDWebImagePrefetcher (по крайней мере, пока он не будет улучшен) и придерживаться своей элементарной техники предварительной выборки. Это не очень сложно, но, похоже, работает.

person Rob    schedule 29.03.2013
comment
Спасибо за этот отличный ответ. Я предлагаю небольшое улучшение этого ответа, например, как Apple выполняет ленивую загрузку в своем примере. ссылка. Почему бы не использовать '- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate' и '- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView' - person Anil Puttabuddhi; 09.11.2013
comment
@AnilPuttabuddhi Согласен, вы можете вызвать это с помощью этих UIScrollViewDelegate методов. Я обновил свой ответ версией, которая не только делает это, но и использует SDWebImagePrefetcher. Я считаю, что это исполнение также лучше обрабатывает несколько разделов. - person Rob; 09.11.2013
comment
Спасибо @Rob за фантастическое решение! - person Alexander Perechnev; 26.11.2013
comment
Отличный пример, спасибо, я принял некоторые части этого кода, однако мне не хватает одной части. Допустим, пользователь щелкнул изображение и перешел в другое представление (очевидно, я прекратил предварительную загрузку), затем, в конце концов, пользователь вернулся в представление коллекции, я хочу снова запустить предварительную загрузку вокруг моего текущего местоположения, однако видимый массив пуст. Не уверен, как вызвать первоначальную предварительную загрузку, когда вид впервые отображается и еще не перетаскивался. Есть идеи? Спасибо! - person Dmitry Fink; 14.03.2014

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

Мое решение довольно чистое, если вы не возражаете (или уже не возражаете) использовать подкласс UITableViewCell:

  1. Подкласс UITableViewCell.
  2. В своем подклассе скройте self.imageView.
  3. Создайте собственное подпредставление UIImageView и установите изображение этого представления.

Вот модифицированная версия моего собственного кода (здесь недокументирована установка рамки в соответствии с размером и положением обложек альбомов приложения iOS Photo):

ВашTableCell.h

@interface YourTableCell : UITableViewCell
    @property (nonatomic, strong) UIImageView *coverPhoto;
@end

ВашTableCell.m

@implementation YourTableCell

@synthesize coverPhoto;

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        self.imageView.image = nil;
        self.coverPhoto = [[UIImageView alloc] init];

        // Any customization, such as initial image, frame bounds, etc. goes here.        

        [self.contentView addSubview:self.coverPhoto];
    }
    return self;
}
//...
@end

ВашTableViewController.m

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";
    YourTableCell *cell = (YourTableCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    //...
    [cell.coverPhoto setImageWithURL:coverUrl placeholderImage:nil options:SDWebImageCacheMemoryOnly];
    //...
}
person Zak Arntson    schedule 10.04.2013

Это пример, и вам нужно реализовать это для вашей цели.
ваш делегат UITableView:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    YourCustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"YourCustomTableViewCellReuseIdentifier"];

    if (!cell)
    {
        cell = [[[YourCustomTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
                               reuseIdentifier:CellIdentifier];        
    }

    NSString *imageURL = // ... get image url, typically from array
    [cell loadImageWithURLString:imageURL forIndexPath:indexPath]; 

    return cell;
}

ваш пользовательский файл UITableViewCell .h:

#import <UIKit/UIKit.h>
#import "UIImageView+WebCache.h"
#import "SDImageCache.h"

@interface YourCustomTableViewCell
{
    NSIndexPath *currentLoadingIndexPath;
}

- (void)loadImageWithURLString:(NSString *)urlString forIndexPath:(NSIndexPath *)indexPath;

@end

ваш пользовательский файл UITableViewCell .m:

// ... some other methods

- (void)loadImageWithURLString:(NSString *)urlString forIndexPath:(NSIndexPath *)indexPath
{
    currentLoadingIndexPath = indexPath;
    [self.imageView cancelCurrentImageLoad];
    [self.imageView setImage:nil];

    NSURL *imageURL = [NSURL URLWithString:urlString];
    [self.imageView setImageWithURL:imageURL
                   placeholderImage:nil
                            options:SDWebImageRetryFailed
                          completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType)
    {
        if (currentLoadingIndexPath != indexPath)
        {
            return;
        }

        if (error)
        {
            ... // handle error
        }
        else
        {
            [imageView setImage:image];
        }
    }];
}

// ... some other methods

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

person Artem Shmatkov    schedule 29.03.2013

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

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

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

[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) {

                         }];

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

Расширенное использование:

//  create customGroup
MQImageDownloadGroup *customGroup = [[MQImageDownloadGroup alloc] initWithGroupIdentifier:@"tableViewCellGroup"];
customGroup.maxConcurrentDownloads = 99;

//  add to MQImageDownloadGroupManage
[[MQImageDownloadGroupManage shareInstance] addGroup:customGroup];

//  use download group
[cell.imageView mq_setImageWithURL:@"https://xxx"
                   groupIdentifier:@"tableViewCellGroup"
                         completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {

                         }];
person maquannene    schedule 21.04.2016