从 UITableView 单元格内的 url 加载异步图像 - 滚动时图像更改为错误图像

从 UITableView 单元格内的 url 加载异步图像 - 滚动时图像更改为错误图像

问题描述:

我编写了两种在 UITableView 单元格中异步加载图片的方法.在这两种情况下,图像都可以正常加载,但是当我滚动表格时,图像会更改几次,直到滚动结束并且图像将返回到正确的图像.我不知道为什么会这样.

I've written two ways to async load pictures inside my UITableView cell. In both cases the image will load fine but when I'll scroll the table the images will change a few times until the scroll will end and the image will go back to the right image. I have no idea why this is happening.

#define kBgQueue dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

- (void)viewDidLoad
{
    [super viewDidLoad];
    dispatch_async(kBgQueue, ^{
        NSData* data = [NSData dataWithContentsOfURL: [NSURL URLWithString:
                                                       @"http://myurl.com/getMovies.php"]];
        [self performSelectorOnMainThread:@selector(fetchedData:)
                               withObject:data waitUntilDone:YES];
    });
}

-(void)fetchedData:(NSData *)data
{
    NSError* error;
    myJson = [NSJSONSerialization
              JSONObjectWithData:data
              options:kNilOptions
              error:&error];
    [_myTableView reloadData];
}    

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    // Return the number of sections.
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    // Return the number of rows in the section.
    // Usually the number of items in your array (the one that holds your list)
    NSLog(@"myJson count: %d",[myJson count]);
    return [myJson count];
}
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{

        myCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
        if (cell == nil) {
            cell = [[myCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
        }

        dispatch_async(kBgQueue, ^{
        NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://myurl.com/%@.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]]];

            dispatch_async(dispatch_get_main_queue(), ^{
        cell.poster.image = [UIImage imageWithData:imgData];
            });
        });
         return cell;
}

......

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

            myCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
            if (cell == nil) {
                cell = [[myCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
            }
    NSURL* url = [NSURL URLWithString:[NSString stringWithFormat:@"http://myurl.com/%@.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]];
    NSURLRequest* request = [NSURLRequest requestWithURL:url];


    [NSURLConnection sendAsynchronousRequest:request
                                       queue:[NSOperationQueue mainQueue]
                           completionHandler:^(NSURLResponse * response,
                                               NSData * data,
                                               NSError * error) {
                               if (!error){
                                   cell.poster.image = [UIImage imageWithData:data];
                                   // do whatever you want with image
                               }

                           }];
     return cell;
}

假设您正在寻找快速的战术修复,您需要做的是确保单元格图像已初始化并且单元格所在的行仍然可见,例如:

Assuming you're looking for a quick tactical fix, what you need to do is make sure the cell image is initialized and also that the cell's row is still visible, e.g:

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

    cell.poster.image = nil; // or cell.poster.image = [UIImage imageNamed:@"placeholder.png"];

    NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"http://myurl.com/%@.jpg", self.myJson[indexPath.row][@"movieId"]]];

    NSURLSessionTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        if (data) {
            UIImage *image = [UIImage imageWithData:data];
            if (image) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    MyCell *updateCell = (id)[tableView cellForRowAtIndexPath:indexPath];
                    if (updateCell)
                        updateCell.poster.image = image;
                });
            }
        }
    }];
    [task resume];

    return cell;
}

以上代码解决了由于单元格被重用而产生的一些问题:

The above code addresses a few problems stemming from the fact that the cell is reused:

  1. 在启动后台请求之前,您没有初始化单元格图像(这意味着在下载新图像时,出列单元格的最后一张图像仍然可见).确保 nil 任何图像视图的 image 属性,否则你会看到图像闪烁.

  1. You're not initializing the cell image before initiating the background request (meaning that the last image for the dequeued cell will still be visible while the new image is downloading). Make sure to nil the image property of any image views or else you'll see the flickering of images.

一个更微妙的问题是,在非常慢的网络上,您的异步请求可能不会在单元格滚出屏幕之前完成.您可以使用 UITableView 方法 cellForRowAtIndexPath:(不要与类似名称的 UITableViewDataSource 方法 tableView:cellForRowAtIndexPath: 混淆>) 查看该行的单元格是否仍然可见.如果单元格不可见,此方法将返回 nil.

A more subtle issue is that on a really slow network, your asynchronous request might not finish before the cell scrolls off the screen. You can use the UITableView method cellForRowAtIndexPath: (not to be confused with the similarly named UITableViewDataSource method tableView:cellForRowAtIndexPath:) to see if the cell for that row is still visible. This method will return nil if the cell is not visible.

问题是当您的异步方法完成时单元格已经滚动,更糟糕的是,该单元格已被重新用于表格的另一行.通过检查该行是否仍然可见,您可以确保不会意外地使用已滚动出屏幕的行的图像更新图像.

The issue is that the cell has scrolled off by the time your async method has completed, and, worse, the cell has been reused for another row of the table. By checking to see if the row is still visible, you'll ensure that you don't accidentally update the image with the image for a row that has since scrolled off the screen.

与手头的问题有些无关,我仍然觉得有必要更新它以利用现代约定和 API,特别是:

Somewhat unrelated to the question at hand, I still felt compelled to update this to leverage modern conventions and API, notably:

  • 使用 NSURLSession 而不是分派 -[NSData contentsOfURL:] 到后台队列;

  • Use NSURLSession rather than dispatching -[NSData contentsOfURL:] to a background queue;

使用 dequeueReusableCellWithIdentifier:forIndexPath: 而不是 dequeueReusableCellWithIdentifier:(但确保使用单元原型或注册类或 NIB 作为该标识符);和

Use dequeueReusableCellWithIdentifier:forIndexPath: rather than dequeueReusableCellWithIdentifier: (but make sure to use cell prototype or register class or NIB for that identifier); and

我使用的类名符合 Cocoa 命名约定(即以大写字母开头).

I used a class name that conforms to Cocoa naming conventions (i.e. start with the uppercase letter).

即使进行了这些更正,也存在问题:

Even with these corrections, there are issues:

  1. 上面的代码没有缓存下载的图像.这意味着如果您将图像滚动到屏幕外然后又回到屏幕上,应用程序可能会再次尝试检索该图像.也许你很幸运,你的服务器响应头将允许 NSURLSessionNSURLCache 提供的相当透明的缓存,但如果不是,你将发出不必要的服务器请求并提供更慢的用户体验.

  1. The above code is not caching the downloaded images. That means that if you scroll an image off screen and back on screen, the app may try to retrieve the image again. Perhaps you'll be lucky enough that your server response headers will permit the fairly transparent caching offered by NSURLSession and NSURLCache, but if not, you'll be making unnecessary server requests and offering a much slower UX.

我们不会取消对滚动到屏幕外的单元格的请求.因此,如果您快速滚动到第 100 行,则该行的图像可能会积压在前 99 行甚至不再可见的请求之后.您总是希望确保对可见单元格的请求进行优先级排序,以获得最佳用户体验.

We're not canceling requests for cells that scroll off screen. Thus, if you rapidly scroll to the 100th row, the image for that row could be backlogged behind requests for the previous 99 rows that aren't even visible anymore. You always want to make sure you prioritize requests for visible cells for the best UX.

解决这些问题的最简单的方法是使用 UIImageView 类别,例如 SDWebImageAFNetworking.如果你愿意,你可以自己写代码来处理上面的问题,但是工作量很大,上面的UIImageView类已经为你做好了.

The simplest fix that addresses these issues is to use a UIImageView category, such as is provided with SDWebImage or AFNetworking. If you want, you can write your own code to deal with the above issues, but it's a lot of work, and the above UIImageView categories have already done this for you.