ios 使用 GCD 异步下载 UITableView 的图像

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/15668160/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-30 22:54:45  来源:igfitidea点击:

Asynchronous downloading of images for UITableView with GCD

iosobjective-cios6

提问by Dvole

I'm downloading images for my uitableview asynchronously using GCD, but there is a problem - when scrolling images flicker and change all the time. I tried setting image to nil with every cell, but it doesn't help much. When scrolling back up fast, all images are wrong. What can I do about that? Here is my method for cells:

我正在使用 GCD 为我的 uitableview 异步下载图像,但是有一个问题 - 当滚动图像闪烁并一直更改时。我尝试将每个单元格的图像设置为 nil,但这并没有多大帮助。快速向上滚动时,所有图像都是错误的。我该怎么办?这是我的细胞方法:

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

    if (self.loader.parsedData[indexPath.row] != nil)
    {
        cell.imageView.image = nil;
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
            dispatch_async(queue, ^(void) {

                NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[self.loader.parsedData[indexPath.row] objectForKey:@"imageLR"]]];

                UIImage* image = [[UIImage alloc] initWithData:imageData];

                dispatch_async(dispatch_get_main_queue(), ^{
                    cell.imageView.image = image;
                    [cell setNeedsLayout];
                     });
            });

    cell.textLabel.text = [self.loader.parsedData[indexPath.row] objectForKey:@"id"];
    }
    return cell;
}

回答by Seamus Campbell

The problem here is that your image-fetching blocks are holding references to the tableview cells. When the download completes, it sets the imageView.imageproperty, even if you have recycled the cell to display a different row.

这里的问题是您的图像获取块持有对 tableview 单元格的引用。下载完成后,它会设置该imageView.image属性,即使您已回收单元格以显示不同的行。

You'll need your download completion block to test whether the image is still relevant to the cell before setting the image.

在设置图像之前,您需要下载完成块来测试图像是否仍然与单元格相关。

It's also worth noting that you're not storing the images anywhere other than in the cell, so you'll be downloading them again each time you scroll a row onscreen. You probably want to cache them somewhere and look for locally cached images before starting a download.

还值得注意的是,您不会将图像存储在单元格以外的任何地方,因此每次在屏幕上滚动一行时,您都会再次下载它们。您可能希望将它们缓存在某处并在开始下载之前查找本地缓存的图像。

Edit: here's a simple way to test, using the cell's tagproperty:

编辑:这是一种使用单元格tag属性的简单测试方法:

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

    cell.tag = indexPath.row;
    NSDictionary *parsedData = self.loader.parsedData[indexPath.row];
    if (parsedData)
    {
        cell.imageView.image = nil;
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
        dispatch_async(queue, ^(void) {

            NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:parsedData[@"imageLR"]];

            UIImage* image = [[UIImage alloc] initWithData:imageData];
            if (image) {
                 dispatch_async(dispatch_get_main_queue(), ^{
                     if (cell.tag == indexPath.row) {
                         cell.imageView.image = image;
                         [cell setNeedsLayout];
                     }
                 });
             }
        });

        cell.textLabel.text = parsedData[@"id"];
    }
    return cell;
}

回答by Hermann Klecker

The point is that you did not fully understand the cell reusing concept. That does not agree very well with asynchronous downloads.

关键是你没有完全理解细胞重用的概念。这与异步下载不太一致。

The block

    ^{
    cell.imageView.image = image;
    [cell setNeedsLayout];
}

gets executed when the request is finished and all data was loaded. But cell gets its value when the block is created.

当请求完成并加载所有数据时执行。但是当块被创建时,cell 就会得到它的值。

By the time when the block is executed cell still points to one of the existing cells. But it is quite likely that the user continued scrolling. The cell object was re-used in the meantime and the image is associated with an 'old' cell that is reused and assigned and displayed. Shortly after that the correct image is loaded and assigned and displayed unless the user has scrolled further. and so on and so on.

到执行块时,单元格仍指向现有单元格之一。但很有可能用户继续滚动。在此期间,单元格对象被重新使用,并且图像与一个“”单元格相关联,该单元格被重新使用、分配和显示。不久之后,正确的图像被加载、分配和显示,除非用户进一步滚动。等等等等。

You should look for a smarter way of doing that. There are lots of turorials. Google for lazy image loading.

你应该寻找一种更聪明的方法来做到这一点。有很多turorials。谷歌用于延迟图像加载。

回答by Carl Veazey

Use the index path to get the cell. If it's not visible the cell will be niland you won't have an issue. Of course you'll probably want to cache the data when it's downloaded so as to set the cell's image immediately when you have the image already.

使用索引路径获取单元格。如果它不可见,则单元格将可见nil,您将不会遇到问题。当然,您可能希望在下载数据时缓存数据,以便在您已经拥有图像时立即设置单元格的图像。

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

    if (self.loader.parsedData[indexPath.row] != nil)
    {
        cell.imageView.image = nil;
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
            dispatch_async(queue, ^(void) {
                //  You may want to cache this explicitly instead of reloading every time.
                NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:[self.loader.parsedData[indexPath.row] objectForKey:@"imageLR"]]];
                UIImage* image = [[UIImage alloc] initWithData:imageData];
                dispatch_async(dispatch_get_main_queue(), ^{
                    // Capture the indexPath variable, not the cell variable, and use that
                    UITableViewCell *blockCell = [tableView cellForRowAtIndexPath:indexPath];
                    blockCell.imageView.image = image;
                    [blockCell setNeedsLayout];
                });
            });
        cell.textLabel.text = [self.loader.parsedData[indexPath.row] objectForKey:@"id"];
    }

    return cell;
}

回答by Thomás Calmon

I've been studying about this problem and I found an excellent approach by customizing an UITableViewCell.

我一直在研究这个问题,并通过自定义 UITableViewCell 找到了一个很好的方法。

#import <UIKit/UIKit.h>

@interface MyCustomCell : UITableViewCell

@property (nonatomic, strong) NSURLSessionDataTask *imageDownloadTask;
@property (nonatomic, weak) IBOutlet UIImageView *myImageView;
@property (nonatomic, weak) IBOutlet UIActivityIndicatorView *activityIndicator;

@end

Now, in your TableViewController, declare two properties for NSURLSessionConfiguration and NSURLSession and initialize them in ViewDidLoad:

现在,在您的 TableViewController 中,为 NSURLSessionConfiguration 和 NSURLSession 声明两个属性,并在 ViewDidLoad 中初始化它们:

@interface MyTableViewController ()

@property (nonatomic, strong) NSURLSessionConfiguration *sessionConfig;
@property (nonatomic, strong) NSURLSession *session;
.
.
.
@end

@implementation TimesVC

- (void)viewDidLoad
{
    [super viewDidLoad];

    _sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
    _session = [NSURLSession sessionWithConfiguration:_sessionConfig];
}

.
.
.

Let's supose that your datasource is a NSMutableDictionary's array (or NSManagedObjectContext). You can easilly download image for each cell, with caching, like that:

让我们假设您的数据源是 NSMutableDictionary 的数组(或 NSManagedObjectContext)。您可以通过缓存轻松下载每个单元格的图像,如下所示:

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

    if (!cell)
    {
        cell = [[MyCustomCell alloc] initWithStyle:UITableViewCellStyleDefault
                                reuseIdentifier:@"cell"];
    }

    NSMutableDictionary *myDictionary = [_myArrayDataSource objectAtIndex:indexPath.row];    

    if (cell.imageDownloadTask)
    {
        [cell.imageDownloadTask cancel];
    }

    [cell.activityIndicator startAnimating];
    cell.myImageView.image = nil;

    if (![myDictionary valueForKey:@"image"])
    {
        NSString *urlString = [myDictionary valueForKey:@"imageURL"];
        NSURL *imageURL = [NSURL URLWithString:urlString];
        if (imageURL)
        {
            cell.imageDownloadTask = [_session dataTaskWithURL:imageURL
                completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
            {
                if (error)
                {
                    NSLog(@"ERROR: %@", error);
                }
                else
                {
                    NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;

                    if (httpResponse.statusCode == 200)
                    {
                        UIImage *image = [UIImage imageWithData:data];

                        dispatch_async(dispatch_get_main_queue(), ^{
                            [myDictionary setValue:data forKey:@"image"];
                            [cell.myImageView setImage:image];
                            [cell.activityIndicator stopAnimating];
                        });
                    }
                    else
                    {
                        NSLog(@"Couldn't load image at URL: %@", imageURL);
                        NSLog(@"HTTP %d", httpResponse.statusCode);
                    }
                }
            }];

            [cell.imageDownloadTask resume];
        }
    }
    else
    {
        [cell.myImageView setImage:[UIImage imageWithData:[myDictionary valueForKey:@"image"]]];
        [cell.activityIndicator stopAnimating];
    }

    return cell;
}

I hope it helps some devs! Cheers.

我希望它可以帮助一些开发人员!干杯。

CREDITS: Table View Images in iOS 7

积分:iOS 7 中的表格视图图像

回答by halbano

Don't reinvent the wheel, just use this couple of awesome pods:

不要重新发明轮子,只需使用这两个很棒的 pod:

https://github.com/rs/SDWebImage

https://github.com/rs/SDWebImage

https://github.com/JJSaccolo/UIActivityIndicator-for-SDWebImage

https://github.com/JJSaccolo/UIActivityIndi​​cator-for-SDWebImage

As simple as:

就这么简单:

    [self.eventImage setImageWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@/%@", [SchemeConfiguration APIEndpoint] , _event.imageUrl]]
                          placeholderImage:nil
                                 completed:^(UIImage *image,
                                             NSError *error,
                                             SDImageCacheType cacheType,
                                             NSURL *imageURL)
     {
         event.image = UIImagePNGRepresentation(image);
     } usingActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];

回答by Sazzad Hissain Khan

Beside accepted answer of Seamus Campbellyou should also know that sometime this does not work. In that case we should reload the specific cell. So

除了Seamus Campbell 的公认答案之外,您还应该知道有时这是行不通的。在这种情况下,我们应该重新加载特定的单元格。所以

if (image) {
     dispatch_async(dispatch_get_main_queue(), ^{
          if (cell.tag == indexPath.row) {
               cell.imageView.image = image;
               [cell setNeedsLayout];
          }
      });
 }

should be changed into,

应该改成,

    if (image) {
         dispatch_async(dispatch_get_main_queue(), ^{
              if (cell.tag == indexPath.row) {
                   cell.imageView.image = image;
                   self.tableView.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
              }
          });
     }

回答by RockyEEKlvn

Have you thought of using SDWebImage? It downloads the image from the given URL asynchronously also. I didn't use the entire library (imported only UIImageView+WebCache.h). Once imported, all you have to do is call the method as such:

你有没有想过使用 SDWebImage?它也从给定的 URL 异步下载图像。我没有使用整个库(仅导入 UIImageView+WebCache.h)。导入后,您所要做的就是调用该方法:

[UIImageXYZ sd_setImageWithURL:["url you're retrieving from"] placeholderImage:[UIImage imageNamed:@"defaultProfile.jpeg"]];

It might be overkill if you're using AFNetworking 2.0, but it worked out for me.

如果您使用 AFNetworking 2.0,这可能有点过分,但对我来说很有效。

Here is the link to the github if you want to give it a try

如果你想试一试,这里是 github 的链接

回答by Ronak Vora

For those worrying about index paths changing and not being consistent, a potential solution is to subclass UITableViewCell or UICollectionViewCell and add an instance variable called something like stringTag. Then, put the URL of the photo you are downloading in the stringTag. When setting the image, check if the stringTag is still the right URL.

对于那些担心索引路径改变和不一致的人来说,一个潜在的解决方案是继承 UITableViewCell 或 UICollectionViewCell 并添加一个名为 stringTag 之类的实例变量。然后,将您正在下载的照片的 URL 放在 stringTag 中。设置图像时,检查 stringTag 是否仍然是正确的 URL。

See this answer for more detail: Asynchronous Fetch completed: Is cell still being displayed?.

有关更多详细信息,请参阅此答案:异步获取已完成:是否仍在显示单元格?.

Here are my classes:

这是我的课程:

import Foundation
import UIKit

class ImageAsyncCollectionViewCell : UICollectionViewCell {
  var stringTag: String?
}

and then when using the cell:

然后在使用单元格时:

    cell.stringTag = photoKey
    cell.imageView.image = self.blankImage
    if ImageCache.default.imageCachedType(forKey: photoKey).cached {
      ImageCache.default.retrieveImage(forKey: photoKey, options: nil) {
        image, cacheType in
        if let image = image {
          DispatchQueue.main.async {
            if cell.stringTag == photoKey {
              cell.imageView.image = image
            }
          }
        } else {
          print("Doesn't exist in cache, but should")
          self.setCellWithPhoto(photoKey: photoKey, cell: cell)
        }
      }
    } else {
      self.setCellWithPhoto(photoKey: photoKey, cell: cell)
    }