从 UITableView 单元格内的 URL 加载异步图像-滚动时图像变成了错误的图像

我已经写了两种在 UITableView 单元中异步加载图片的方法。在这两种情况下,图像将载入良好,但当我将滚动表的图像将改变几次,直到滚动将结束,图像将返回到正确的图像。我不知道为什么会这样。

#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;
}
143741 次浏览

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

- (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;
}

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

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

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

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

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

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

    • 使用 dequeueReusableCellWithIdentifier:forIndexPath:而不是 dequeueReusableCellWithIdentifier:(但要确保使用单元原型、寄存器类或 NIB 作为该标识符) ; 以及

    • 我使用了一个符合 可可命名约定的类名(即以大写字母开头)。

即便进行了这些修正,仍然存在一些问题:

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

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

解决这些问题的最简单的修复方法是使用 UIImageView类别,例如 SDWebImageAFNetworking提供的类别。如果您愿意,您可以编写自己的代码来处理上述问题,但这是一项繁重的工作,而且上述 UIImageView类别已经为您完成了这项工作。

谢谢你“罗布”... 我有同样的问题与 UICollectionView 和你的答案帮助我解决了我的问题。 这是我的代码:

 if ([Dict valueForKey:@"ImageURL"] != [NSNull null])
{
cell.coverImageView.image = nil;
cell.coverImageView.imageURL=nil;


dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{


if ([Dict valueForKey:@"ImageURL"] != [NSNull null] )
{
dispatch_async(dispatch_get_main_queue(), ^{


myCell *updateCell = (id)[collectionView cellForItemAtIndexPath:indexPath];


if (updateCell)
{
cell.coverImageView.image = nil;
cell.coverImageView.imageURL=nil;


cell.coverImageView.imageURL=[NSURL URLWithString:[Dict valueForKey:@"ImageURL"]];


}
else
{
cell.coverImageView.image = nil;
cell.coverImageView.imageURL=nil;
}




});
}
});


}
else
{
cell.coverImageView.image=[UIImage imageNamed:@"default_cover.png"];
}

我认为你想加快你的细胞载入图像载入时,为细胞在背景。为此,我们采取了以下步骤:

  1. 检查文档目录中是否存在该文件。

  2. 如果没有,然后加载图像的第一次,并保存到 我们的电话文件目录。如果你不想在手机中保存图像,那么你可以直接在背景中加载单元格图像。

  3. 现在加载过程:

只包括: #import "ManabImageOperations.h"

单元格的代码如下:

NSString *imagestr=[NSString stringWithFormat:@"http://www.yourlink.com/%@",[dictn objectForKey:@"member_image"]];


NSString *docDir=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)objectAtIndex:0];
NSLog(@"Doc Dir: %@",docDir);


NSString  *pngFilePath = [NSString stringWithFormat:@"%@/%@",docDir,[dictn objectForKey:@"member_image"]];


BOOL fileExists = [[NSFileManager defaultManager] fileExistsAtPath:pngFilePath];
if (fileExists)
{
[cell1.memberimage setImage:[UIImage imageWithContentsOfFile:pngFilePath] forState:UIControlStateNormal];
}
else
{
[ManabImageOperations processImageDataWithURLString:imagestr andBlock:^(NSData *imageData)
{
[cell1.memberimage setImage:[[UIImage alloc]initWithData: imageData] forState:UIControlStateNormal];
[imageData writeToFile:pngFilePath atomically:YES];
}];
}

ManabImageOperations.h:

#import <Foundation/Foundation.h>


@interface ManabImageOperations : NSObject
{
}
+ (void)processImageDataWithURLString:(NSString *)urlString andBlock:(void (^)(NSData *imageData))processImage;
@end

ManabImageOperations.m:

#import "ManabImageOperations.h"
#import <QuartzCore/QuartzCore.h>
@implementation ManabImageOperations


+ (void)processImageDataWithURLString:(NSString *)urlString andBlock:(void (^)(NSData *imageData))processImage
{
NSURL *url = [NSURL URLWithString:urlString];


dispatch_queue_t callerQueue = dispatch_get_main_queue();
dispatch_queue_t downloadQueue = dispatch_queue_create("com.myapp.processsmagequeue", NULL);
dispatch_async(downloadQueue, ^{
NSData * imageData = [NSData dataWithContentsOfURL:url];


dispatch_async(callerQueue, ^{
processImage(imageData);
});
});
//  downloadQueue=nil;
dispatch_release(downloadQueue);


}
@end

如果有任何问题发生,请检查答案并发表评论..。

/* 我已经这样做了,也测试了它 */

步骤1 = 在 viewDidLoad 方法中为这样的表注册自定义单元格类(对于表中的原型单元格)或 nib (对于自定义单元格的自定义 nib) :

[self.yourTableView registerClass:[CustomTableViewCell class] forCellReuseIdentifier:@"CustomCell"];

或者

[self.yourTableView registerNib:[UINib nibWithNibName:@"CustomTableViewCell" bundle:nil] forCellReuseIdentifier:@"CustomCell"];

步骤2 = 像下面这样使用 UITableView 的“ dequeue eReusableCellWithIdentifier: forIndexPath:”方法(为此,必须注册 class 或 nib) :

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


cell.imageViewCustom.image = nil; // [UIImage imageNamed:@"default.png"];
cell.textLabelCustom.text = @"Hello";


dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// retrive image on global queue
UIImage * img = [UIImage imageWithData:[NSData dataWithContentsOfURL:     [NSURL URLWithString:kImgLink]]];


dispatch_async(dispatch_get_main_queue(), ^{


CustomTableViewCell * cell = (CustomTableViewCell *)[tableView cellForRowAtIndexPath:indexPath];
// assign cell image on main thread
cell.imageViewCustom.image = img;
});
});


return cell;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
Static NSString *CellIdentifier = @"Cell";
QTStaffViewCell *cell = (QTStaffViewCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];


If (cell == nil)
{


NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"QTStaffViewCell" owner:self options:nil];
cell = [nib objectAtIndex: 0];


}


StaffData = [self.staffArray objectAtIndex:indexPath.row];
NSString *title = StaffData.title;
NSString *fName = StaffData.firstname;
NSString *lName = StaffData.lastname;


UIFont *FedSanDemi = [UIFont fontWithName:@"Aller" size:18];
cell.drName.text = [NSString stringWithFormat:@"%@ %@ %@", title,fName,lName];
[cell.drName setFont:FedSanDemi];


UIFont *aller = [UIFont fontWithName:@"Aller" size:14];
cell.drJob.text = StaffData.job;
[cell.drJob setFont:aller];


if ([StaffData.title isEqualToString:@"Dr"])
{
cell.drJob.frame = CGRectMake(83, 26, 227, 40);
}
else
{
cell.drJob.frame = CGRectMake(90, 26, 227, 40);


}


if ([StaffData.staffPhoto isKindOfClass:[NSString class]])
{
NSURL *url = [NSURL URLWithString:StaffData.staffPhoto];
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDownloadTask *task = [session downloadTaskWithURL:url
completionHandler:^(NSURL *location,NSURLResponse *response, NSError *error) {


NSData *imageData = [NSData dataWithContentsOfURL:location];
UIImage *image = [UIImage imageWithData:imageData];


dispatch_sync(dispatch_get_main_queue(),
^{
cell.imageView.image = image;
});
}];
[task resume];
}
return cell;}

最好的答案不是这样做的正确方法:。您实际上使用模型绑定 indexPath,这并不总是好的。假设在加载图像期间添加了一些行。现在屏幕上存在给定 indexPath 的单元格,但图像不再正确!这种情况有点不太可能,也很难复制,但它是可能的。

最好使用 MVVM 方法,在控制器中将单元与 viewModel 绑定,在 viewModel 中加载图像(使用 switchToLatest 方法分配 ReactiveCocoa 信号) ,然后订阅该信号并将图像分配给单元!;)

你必须记住不要滥用 MVVM。观点必须非常简单!而 ViewModel 应该是可重用的!这就是为什么在控制器中绑定 View (UITableViewCell)和 ViewModel 非常重要。

有多种框架可以解决这个问题,举几个例子:

斯威夫特:

目标 C:

在我的例子中,这不是由于图像缓存(使用 SDWebImage)。这是因为自定义单元格的标记与 indexPath.row 不匹配。

在 cellForRowAtIndexPath 上:

1)为自定义单元格分配一个索引值,

cell.tag = indexPath.row

2)在主线程上,在分配图像之前,通过与标签匹配来检查图像是否属于相应的单元格。

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

下面是快速版本(通过使用@Nitesh Borad 目标 C 代码) :-

   if let img: UIImage = UIImage(data: previewImg[indexPath.row]) {
cell.cardPreview.image = img
} else {
// The image isn't cached, download the img data
// We should perform this in a background thread
let imgURL = NSURL(string: "webLink URL")
let request: NSURLRequest = NSURLRequest(URL: imgURL!)
let session = NSURLSession.sharedSession()
let task = session.dataTaskWithRequest(request, completionHandler: {data, response, error -> Void in
let error = error
let data = data
if error == nil {
// Convert the downloaded data in to a UIImage object
let image = UIImage(data: data!)
// Store the image in to our cache
self.previewImg[indexPath.row] = data!
// Update the cell
dispatch_async(dispatch_get_main_queue(), {
if let cell: YourTableViewCell = tableView.cellForRowAtIndexPath(indexPath) as? YourTableViewCell {
cell.cardPreview.image = image
}
})
} else {
cell.cardPreview.image = UIImage(named: "defaultImage")
}
})
task.resume()
}

简单的改变,

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];
});
});

进入

    dispatch_async(kBgQueue, ^{
NSData *imgData = [NSData dataWithContentsOfURL:[NSURL URLWithString:   [NSString stringWithFormat:@"http://myurl.com/%@.jpg",[[myJson objectAtIndex:indexPath.row] objectForKey:@"movieId"]]]];
cell.poster.image = [UIImage imageWithData:imgData];
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationNone];
});
});
 - (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;
}

你只需要传递你的网址,

NSURL *url = [NSURL URLWithString:@"http://www.myurl.com/1.png"];
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(), ^{
yourimageview.image = image;
});
}
}
}];
[task resume];

Swift 3

我使用 NSCache 为图像加载器编写了自己的 light 实现。 没有手机图像闪烁!

ImageCacheLoader

typealias ImageCacheLoaderCompletionHandler = ((UIImage) -> ())


class ImageCacheLoader {
    

var task: URLSessionDownloadTask!
var session: URLSession!
var cache: NSCache<NSString, UIImage>!
    

init() {
session = URLSession.shared
task = URLSessionDownloadTask()
self.cache = NSCache()
}
    

func obtainImageWithPath(imagePath: String, completionHandler: @escaping ImageCacheLoaderCompletionHandler) {
if let image = self.cache.object(forKey: imagePath as NSString) {
DispatchQueue.main.async {
completionHandler(image)
}
} else {
/* You need placeholder image in your assets,
if you want to display a placeholder to user */
let placeholder = #imageLiteral(resourceName: "placeholder")
DispatchQueue.main.async {
completionHandler(placeholder)
}
let url: URL! = URL(string: imagePath)
task = session.downloadTask(with: url, completionHandler: { (location, response, error) in
if let data = try? Data(contentsOf: url) {
let img: UIImage! = UIImage(data: data)
self.cache.setObject(img, forKey: imagePath as NSString)
DispatchQueue.main.async {
completionHandler(img)
}
}
})
task.resume()
}
}
}

用法例子

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    

let cell = tableView.dequeueReusableCell(withIdentifier: "Identifier")
    

cell.title = "Cool title"


imageLoader.obtainImageWithPath(imagePath: viewModel.image) { (image) in
// Before assigning the image, check whether the current cell is visible
if let updateCell = tableView.cellForRow(at: indexPath) {
updateCell.imageView.image = image
}
}
return cell
}