Загрузка изображений tableview из URL-адреса в фоновом режиме приводит к дублированию

У меня есть табличное представление, которое заполняется результатами поискового запроса. Многие из результатов содержат изображения, которые необходимо загружать с URL-адресов, но не все. Первоначально у меня было захват изображения из URL-адреса в методе cellForRowAtIndexPath в основном потоке, который работал отлично, но это делало прокрутку табличного представления прерывистой, поскольку оно на мгновение «застревало» на каждом изображении.

Итак, я решил попробовать загрузить изображения в фоновом потоке. Вот мой метод cellForRowAtIndexPath. URL-адреса хранятся в массиве результатов с индексами, соответствующими строке ячейки.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    [sBar resignFirstResponder];
    if (indexPath.row != ([resultsArray count])) 
    {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ResultCell"];
    UIImageView * bookImage = (UIImageView *)[cell viewWithTag:102];
        //set blank immediately so repeats are not shown
        bookImage.image = NULL;

        //get a dispatch queue
        dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
        //this will start the image loading in bg
        dispatch_async(concurrentQueue, ^{        
            NSData *image = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:[[resultsArray objectAtIndex:indexPath.row] thumbnailURL]]];

            //this will set the image when loading is finished
            dispatch_async(dispatch_get_main_queue(), ^{
                bookImage.image = [UIImage imageWithData:image];

                if(bookImage.image == nil)
                            {
                                bookImage.image = [UIImage imageNamed:@"no_image.jpg"];
                            }
            });
        }); 

        return cell;
    }
    // This is for the last cell, which loads 10 additional items when touched
    // Not relevant for this question, I think, but I'll leave it here anyway
    else{
        UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"More"];
        UILabel *detailLabel = (UILabel *)[cell viewWithTag:101];
        detailLabel.text = [NSString stringWithFormat:@"Showing %d of %d results", [resultsArray count], [total intValue]];
        if ([UIApplication sharedApplication].networkActivityIndicatorVisible == NO) {
            cell.userInteractionEnabled = YES;
        }
        return cell;

    }
}

Когда табличное представление прокручивается медленно, все изображения загружаются в соответствующие ячейки, что я могу подтвердить, потому что изображения соответствуют заголовкам ячеек, но я оставил настройку всего, кроме изображения, чтобы сократить его для этого вопроса. Однако, когда я прокручиваю быстрее, особенно когда я имитирую медленное интернет-соединение, изображения начинают загружаться не в те ячейки. Я думаю, что это как-то связано с повторным использованием ячеек, потому что, если я быстро прокручиваю вверх, изображение ячейки, только что покидающей представление, часто оказывается в ячейке, только что вошедшей. Я думал, что bookImage.image = NULL; линия гарантирует, что этого не произойдет, но я думаю, что нет. Думаю, я не очень хорошо понимаю фоновые потоки, когда изображение, наконец, загружается из URL-адреса, не потеряло ли оно отслеживание того, для какой ячейки оно было предназначено? Вы знаете, что может происходить? Спасибо за любой отзыв!


person Ryan    schedule 28.03.2012    source источник


Ответы (3)


По предположению:

  1. Ячейка A загружается и начинает асинхронный запрос A.
  2. Таблица прокручивается, и ячейка A прокручивается за пределы экрана до завершения запроса.
  3. Ячейка A перерабатывается и удаляется из очереди как ячейка B.
  4. Ячейка B начинает асинхронный запрос B.
  5. Асинхронный запрос A завершается, обновляя представление изображения ячейки A (теперь ячейки B).

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

person jnic    schedule 28.03.2012
comment
Хорошо, это логично, спасибо за отзыв! Но как мне отслеживать, какая ячейка удаляется из очереди и какой запрос ей соответствует? - person Ryan; 30.03.2012

Мой подход (полагается на библиотеку AFNetworking). Включите в свой проект классы из следующего zip-файла:

http://dl.dropbox.com/u/6487838/imageloading.zip

Создайте свои собственные пользовательские ячейки. Код может выглядеть примерно так (проверьте методы -setComment: и -willMoveToSuperview:):

#import "CommentCell.h"
#import "CommentCellView.h"
#import "MBImageLoader.h"


@interface CommentCell ()
@property (nonatomic, strong) UIImageView     *imageView;
@property (nonatomic, strong) CommentCellView *cellView;
@property (nonatomic, assign) CellStyle       style;
@end


@implementation CommentCell

@synthesize cellView, style = _style, imageView, comment = _comment, showCounter;

- (id)initWithStyle:(CellStyle)style reuseIdentifier:(NSString *)reuseIdentifier;
{
    self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier];
    if (self)
    {
        self.selectionStyle = UITableViewCellSelectionStyleNone;
        self.opaque = YES;
        self.style = style;
        self.showCounter = YES;


        CGFloat width = 53.0f;
        CGFloat x = (style == CellStyleRight) ? 320.0f - 10.0f - width : 10.0f;
        CGRect frame = CGRectMake(x, 10.0f, width, 53.0f);
        self.imageView = [[UIImageView alloc] initWithFrame:frame];
        imageView.image = [UIImage imageNamed:@"User.png"];
        [self.contentView addSubview:imageView];


        self.cellView = [[CommentCellView alloc] initWithFrame:self.frame cell:self];
        cellView.backgroundColor = [UIColor clearColor];
        [self.contentView addSubview:cellView];
    }
    return self;
}

- (void)dealloc
{
    self.comment = nil;
    self.cellView = nil;
    self.imageView = nil;
}

- (void)willMoveToSuperview:(UIView *)newSuperview
{
    [super willMoveToSuperview:newSuperview];

    if (!newSuperview)
    {
        [[MBImageLoader sharedLoader] cancelLoadImageAtURL:_comment.iconURL forTarget:self];
    }
}

- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
    [super setSelected:selected animated:animated];

    // Configure the view for the selected state
}

- (void)setFrame:(CGRect)newFrame 
{
    [super setFrame:newFrame];

    CGRect bounds = self.bounds;
    bounds.size.height -= 1; 
    cellView.frame = bounds;
}

- (void)setNeedsDisplay 
{
    [super setNeedsDisplay];

    [cellView setNeedsDisplay];
}

- (void)setComment:(MBComment *)comment
{
    if (_comment == comment) 
        return;

    _comment = comment;

    if (comment.icon == nil)
    {
        imageView.image = nil;

        [[MBImageLoader sharedLoader] loadImageForTarget:self withURL:comment.iconURL success:^ (UIImage *image) 
         {
             comment.icon = image;

             imageView.alpha = 0.0f;
             imageView.image = image;

             [UIView animateWithDuration:0.5f delay:0.0f options:UIViewAnimationOptionAllowUserInteraction animations:^ {
                 imageView.alpha = 1.0f;                 
             } completion:^ (BOOL finished) {}];             

         } failure:^ (NSError *error) 
         {
             DLog(@"%@", error);
         }];
    }
    else 
    {
        imageView.image = comment.icon;
    }

    [self setNeedsDisplay];
}

@end
person Wolfgang Schreurs    schedule 29.03.2012


Как я это сделал.

cellForRowAtIndexPath: метод:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath     *)indexPath {

static NSString *cellIdentifier = @"CellIdentificator";

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (cell == nil) {
    cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier] autorelease];
}
[cell.imageView setImage:nil];

NSData *const cachedImageData = [self.cache objectAtIndex:indexPath.row];
if ([cachedImageData isKindOfClass:[NSData class]]) {
    [cell.imageView setImage:[UIImage imageWithData:cachedImageData]];
} else {
    [self downloadAndCacheImageForIndexPath:indexPath];
}

return cell;

}

downloadAndCacheImageForIndexPath: метод:

- (void)downloadAndCacheImageForIndexPath:(NSIndexPath *)indexPath {

dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(concurrentQueue, ^{
    NSString *const imageStringURL = [NSString stringWithFormat:@"%@%.2d.png", IMG_URL, indexPath.row];

    NSData *image = [[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:imageStringURL]];
    [self.cache replaceObjectAtIndex:indexPath.row withObject:image];

    dispatch_async(dispatch_get_main_queue(), ^{
        [self.tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:indexPath.row inSection:0]]withRowAnimation:UITableViewRowAnimationNone];
    });
    [image release];
});

}

self.cache — это NSMutableArray, который я использую для хранения NSData загруженных изображений. self.cache предварительно загружено [NSNull null]s для количества изображений

БР.
Евгений.

person dymv    schedule 29.03.2012