与completionBlock异步下载和保存文件时出现错误的CollectionView单元格图像
Posted
技术标签:
【中文标题】与completionBlock异步下载和保存文件时出现错误的CollectionView单元格图像【英文标题】:Wrong CollectionView cell image while downloading and saving file async with completionBlock 【发布时间】:2014-03-17 13:02:09 【问题描述】:我意识到已经有很多这样的问题,但似乎没有一个可以解决这个问题。
CollectionView 从files
数组读取文件名,然后从 Documents 目录加载图像或下载它们,显示并保存到 Documents。工作非常流畅,没有任何额外的库,但是当快速滚动几个单元格时会收到错误的图像。通过任何操作或向后滚动调用 [collectionView reloadData]
会将这些错误的图像重新加载到好的图像。
我想这与异步图像分配的单元重用有关,但是如何解决这个问题?在 Storyboard 中定义的 CollectionView 和自定义单元格。图像存储在基本身份验证之后的服务器上,因此大多数重用解决方案都不适用。
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
BrowseCollectionViewCell *cell = [self.collectionView dequeueReusableCellWithReuseIdentifier:@"MyCell" forIndexPath:indexPath];
NSString *fileName = [files objectAtIndex:indexPath.item];
NSString *filePath = [thumbDir stringByAppendingPathComponent:fileName];
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath])
cell.imageView.image = [UIImage imageWithContentsOfFile:filePath];
else //DOWNLOAD
NSString *strURL = [NSString stringWithFormat:@"%@%@", THUMBURL, fileName];
NSURL *fileURL = [NSURL URLWithString:strURL];
cell.imageView.image = [UIImage imageNamed:@"placeholder.jpg"];
[self downloadFromURL:fileURL to:filePath
completionBlock:^(BOOL succeeded, UIImage *image)
if (succeeded)
cell.imageView.image = image;
];
cell.imageName = fileName;
return cell;
- (void)downloadFromURL:(NSURL*)url to:(NSString *)filePath completionBlock:(void (^)(BOOL succeeded, UIImage *image))completionBlock
NSString *authStr = [NSString stringWithFormat:@"%@:%@", LOGIN, PASS];
NSData *authData = [authStr dataUsingEncoding:NSASCIIStringEncoding];
NSString *authValue = [authData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadRevalidatingCacheData timeoutInterval:30];
[request setValue:[NSString stringWithFormat:@"Basic %@", authValue] forHTTPHeaderField:@"Authorization"];
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *response, NSData *data, NSError *error)
if (!error)
UIImage *image = [[UIImage alloc] initWithData:data];
[data writeToFile:filePath atomically:NO];
completionBlock(YES, image);
else
completionBlock(NO, nil);
];
我尝试了很多修改,但似乎都没有解决问题。目前,效果是快速滚动期间出现的新单元格被多次更改,其中一些总是以错误的图像结束,指向一些重用问题。
提前感谢您的任何帮助和建议。
【问题讨论】:
【参考方案1】:问题在于,当快速滚动时,单元格可能会在异步请求完成时出列并重新用于另一个单元格,从而更新错误的单元格。因此,在完成块中,您应该确保单元格仍然可见:
[self downloadFromURL:fileURL to:filePath completionBlock:^(BOOL succeeded, UIImage *image)
if (succeeded)
BrowseCollectionViewCell *updateCell = (id)[collectionView cellForItemAtIndexPath:indexPath];
if (updateCell) // if the cell is still visible ...
updateCell.imageView.image = image; // ... then update its image
];
注意,此UICollectionView
方法cellForItemAtIndexPath
不应与名称相似的UICollectionViewDataSource
方法collectionView:cellForItemAtIndexPath:
混淆。 UICollectionView
方法 cellForItemAtIndexPath
如果单元格仍然可见,则返回 UICollectionViewCell
,如果不可见,则返回 nil
。
顺便说一句,以上假设此单元格的NSIndexPath
无法更改(即,在异步检索图像时,不可能在该行上方插入其他行)。这有时不是一个有效的假设。所以,如果你想小心点,你真正应该做的是返回模型并重新计算此模型对象的 NSIndexPath
,并在确定适当的 updateCell
引用时使用它。
理想情况下,一旦您解决了上述问题,您可以对此过程进行一些优化:
如果在前一个请求仍在运行时重复使用该单元格,您可能希望取消该前一个请求。如果您不这样做并且快速滚动过去,例如 200 个单元格,现在显示例如单元格 201 到 221,则对这 20 个当前可见单元格的请求将排队,并且在前 200 个请求完成之前不会显示。
不过,为了能够取消之前的请求,您不能使用sendAsynchronousRequest
。您必须使用基于委托的 NSURLConnection
或 NSURLSessionTask
,它们是可取消的。
您可能应该缓存您的图像。如果您当前可见的单元格都有它们的图像,然后您将它们滚动并重新打开,您似乎正在重新请求它们。您应该首先查看您是否已经检索到该图像,如果是,请使用该图像,并且仅当您的缓存中没有图像时,才重新发出请求。
是的,我知道您正在使用持久存储中的文件版本进行缓存,但您也可能希望缓存到 RAM。通常人们为此使用NSCache
。
这需要改变很多。如果您需要帮助,请告诉我们。但更简单的是使用SDWebImage 或AFNetworking 中的UIImageView
类别,它会为您完成所有这些工作。
【讨论】:
谢谢,它有效!广告 1. 我遇到了 NSURLConnection 的一些问题——我认为这是在滚动时更新单元格图像的问题。广告 2。我会调查一下,谢谢。 @yosh 非常好。关于“广告 1”,我不是建议使用基于委托的NSURLConnection
来解决您的原始问题,而是提出一个完全不相关的观察,即它会启用可取消的请求,并且如果您取消对已滚动的单元格的待处理请求看来,如果您在缓慢的 Internet 连接下快速滚动,它将大大提高性能。很抱歉,如果这种不请自来的观察引起了混乱!
无需道歉。我相信我理解,只是不确定如何实现也能满足上述功能的 NSURLConnection,因为我之前遇到过一些问题。
@yosh 是的,这有点复杂。不是火箭科学,但也很重要。这就是为什么我建议 SDWebImage 的UIImageView
类别的原因,因为他们在相当优雅的实现方面做得非常好。 (我是 AFNetworking 的粉丝,但 SDWebImage 的 UIImageView
类别比 AFNetworking 的强一点,恕我直言,所以除非您已经出于其他原因使用 AFNetworking,否则 SDWebImage 可能是要走的路。)
它对我不起作用。我宁愿使用类似的东西 if cell.id == imageUrl 来检查单元格是否与图像异步加载开始的单元格相同。【参考方案2】:
您需要在某处保留一些上下文信息。换句话说,在您的完成块中,检查单元格是否真的应该包含下载的图像。如果没有,请不要分配它。例如图像 URL。你可以这样做...
将属性NSURL *imageURL
添加到您的单元格对象
在您的downloadFromURL:...
存储fileURL
到imageURL
单元格属性之前
在完成块中,比较fileURL
是否匹配cell.imageURL
,如果匹配,则设置图像,否则不设置
和nil
你的单元格imageURL
prepareForReuse
中的属性
...或者您可以将您的加载代码移动到您的单元格类中并在那里分配图像的 URL 等。很多方法可以做到这一点。
【讨论】:
效果很好,谢谢!我使用 imageName 属性进行比较,它解决了这个问题。 或者使用@Rob方式。并添加他的建议(取消、缓存……)。 @robertvojta 我看到 AFNetworking 检查相同的 URL,而 SDWebImage 没有 节省了我一周(一天零 1/2)的时间,因为我将头撞在 cellForItemAtIndexPath 上,对于(非常)可见的单元格返回 nil。我希望这个答案很快就会关闭,以防止无休止的感谢;-)))))))))))))) 这里也一样。非常感谢!【参考方案3】:您需要在块替换中获取单元格的引用:
[self downloadFromURL:fileURL to:filePath
completionBlock:^(BOOL succeeded, UIImage *image)
if (succeeded)
cell.imageView.image = image;
];
与
[self downloadFromURL:fileURL to:filePath
completionBlock:^(BOOL succeeded, UIImage *image)
BrowseCollectionViewCell *innerCell = (BrowseCollectionViewCell *)[collectionView cellForItemAtIndexPath:indexPath];
if (succeeded)
innerCell.imageView.image = image;
];
【讨论】:
以上是关于与completionBlock异步下载和保存文件时出现错误的CollectionView单元格图像的主要内容,如果未能解决你的问题,请参考以下文章