Ios 下载和保存与completionBlock异步的文件时错误的CollectionView单元格图像

Ios 下载和保存与completionBlock异步的文件时错误的CollectionView单元格图像,ios,objective-c,uicollectionview,uicollectionviewcell,Ios,Objective C,Uicollectionview,Uicollectionviewcell,我知道已经有很多这样的问题了,但似乎没有一个能解决这个问题 CollectionView从文件数组中读取文件名,然后从文档目录加载图像或下载图像,显示并保存到文档中。工作非常顺利,没有任何额外的库,但当滚动快速几个细胞收到错误的图像。通过任何操作调用[collectionView reloadData],或向后滚动将这些错误的图像重新加载到正确的图像 我想这与异步映像分配的单元重用有关,但是如何解决这个问题呢?故事板中定义的CollectionView和自定义单元格。图像存储在基本身份验证之后的

我知道已经有很多这样的问题了,但似乎没有一个能解决这个问题

CollectionView从
文件
数组中读取文件名,然后从文档目录加载图像或下载图像,显示并保存到文档中。工作非常顺利,没有任何额外的库,但当滚动快速几个细胞收到错误的图像。通过任何操作调用
[collectionView reloadData]
,或向后滚动将这些错误的图像重新加载到正确的图像

我想这与异步映像分配的单元重用有关,但是如何解决这个问题呢?故事板中定义的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);
         }
     }];
}
我尝试了许多修改,但似乎没有一个能解决问题。目前,其效果是,在快速滚动过程中出现的新单元格被更改了不止一次,其中一些单元格总是以错误的图像结束,这表明存在一些重用问题


提前感谢您的帮助和建议。

您需要在某个地方保留一些上下文信息。换句话说,在完成块中,检查单元格是否真的包含下载的图像。如果没有,请不要分配它。例如图像URL。你可以这样做

  • 将属性
    NSURL*imageURL
    添加到单元格对象
  • 在下载单元格的
    属性之前,请从URL:…
    文件URL
    存储到
    图像URL
  • 在完成块中,比较
    fileURL
    是否匹配
    cell.imageURL
    ,如果匹配,则设置图像,否则不设置图像
  • nil
    prepareforeuse

。。。或者,您可以将加载代码移动到cell类中,并在其中仅分配图像的URL等。执行此操作的方法有很多。

问题是,当快速滚动时,异步请求完成时,该单元格可能会退出队列并重新用于另一个单元格,从而更新错误的单元格。因此,在完成块中,应确保单元格仍然可见:

[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

顺便说一下,上面假定此单元格的
nsindepath
不能更改(即,在异步检索图像时,不可能在此行上方插入其他行)。这有时不是一个有效的假设。因此,如果您想小心,您真正应该做的是返回到模型并重新计算此模型对象的
nsindepath
,并在确定适当的
updateCell
引用时使用它


理想情况下,一旦解决了上述问题,您可以对此流程进行一些优化:

  • 如果在前一个请求仍在运行时重新使用该单元格,则可能需要取消前一个请求。如果您不这样做,并且快速滚动过去,比如说,200个单元格,现在显示,比如说,单元格201到221,那么对这20个当前可见单元格的请求将排队,直到前200个请求完成后才会显示

    但是,为了能够取消以前的请求,您不能使用
    sendAsynchronousRequest
    。您必须使用基于委托的
    NSURLConnection
    NSURLSessionTask
    ,这是可取消的

  • 您可能应该缓存您的图像。如果您当前可见的单元格都有图像,然后您将其滚动开或关上,则表明您正在重新请求它们。您应该首先查看是否已经检索到该图像,如果已经检索到,请使用该图像,并且只有在缓存中没有图像时,才重新发出请求

    是的,我知道您正在使用持久性存储中的文件版本进行缓存,但您可能也希望缓存到RAM。通常人们使用
    NSCache
    来实现此目的


  • 这需要改变很多。如果您需要帮助,请告诉我们。但在or中使用
    UIImageView
    类别要容易得多,它可以为您完成所有这些工作。

    您需要获取块替换中单元格的引用:

    [self downloadFromURL:fileURL to:filePath
                  completionBlock:^(BOOL succeeded, UIImage *image) {
                      if (succeeded)
                      {                          
                          cell.imageView.image = image;
                      }
                  }];
    


    非常好,谢谢!我使用imageName属性进行比较,它解决了这个问题。或者使用@Rob way。并添加他建议的内容(取消、缓存等)。@robertvojta我看到AFNetworking检查相同的URL,而SDWebImage并没有节省我一周(一天半)的时间,因为cellForItemAtIndexPath对(非常)可见的单元格返回零。我希望这个答案很快就会被关闭,以避免无休止的感谢;-鼃)()()()()()())彼此彼此。非常感谢你!谢谢你,这很有效!公元1年。我在NSURLConnection中遇到了一些问题-我认为是在仍然滚动时更新单元格图像的问题。公元2年。我会调查的,谢谢。@yosh很好。关于“ad 1”,我不是建议使用基于委托的
    NSURLConnection
    来解决您最初的问题,而是提出一个完全无关的观察结果,即它将启用可取消的请求,如果您取消已滚动出视图的单元格的挂起请求,如果你在缓慢的互联网连接下快速滚动,将会大大提高性能。抱歉,如果这是一个自发的观察是一个混乱的来源!没必要道歉。我相信我理解,只是不确定如何实现同样满足上述功能的NSURLConnection,因为我有一些专业知识
     [self downloadFromURL:fileURL to:filePath
                  completionBlock:^(BOOL succeeded, UIImage *image) {
    BrowseCollectionViewCell *innerCell = (BrowseCollectionViewCell *)[collectionView cellForItemAtIndexPath:indexPath];
                      if (succeeded)
                      {                          
                          innerCell.imageView.image = image;
                      }
                  }];