使用NSCache和dispatch_async在可重用的表格单元中的正确方式是什么?

11

我一直在寻找一种清晰简明的方法来做这件事,但没有找到任何地方会给一个例子并且解释得非常好。希望你能帮助我。

这是我正在使用的代码:

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

static NSString *CellIdentifier = @"NewsCell";
NewsCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

// Configure the cell...

NewsItem *item = [newsItemsArray objectAtIndex:indexPath.row];

cell.newsTitle.text = item.title;

NSCache *cache = [_cachedImages objectAtIndex:indexPath.row];

[cache setName:@"image"];
[cache setCountLimit:50];

UIImage *currentImage = [cache objectForKey:@"image"];

if (currentImage) {
    NSLog(@"Cached Image Found");
    cell.imageView.image = currentImage;
}else {
    NSLog(@"No Cached Image");

    cell.newsImage.image = nil;

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^(void)
                   {
                       NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:item.image]];
                       dispatch_async(dispatch_get_main_queue(), ^(void)
                       {
                           cell.newsImage.image = [UIImage imageWithData:imageData];
                           [cache setValue:[UIImage imageWithData:imageData] forKey:@"image"];
                           NSLog(@"Record String = %@",[cache objectForKey:@"image"]);
                       });
                   });
}

return cell;
}

缓存对我返回了 nil。


1
你可能想看看SDWebImage项目,它已经解决了这个问题。 - Aaron Brager
你能提供一个链接吗? - mcphersonjr
2
https://github.com/rs/SDWebImage - Rob
2个回答

15

Nitin很好地回答了有关如何使用缓存的问题。问题在于,原始问题和Nitin的答案都存在一个问题,即您正在使用GCD,它(a)不能控制并发请求的数量; (b)调度的块无法取消。此外,您正在使用不可取消的dataWithContentsOfURL

请参阅WWDC 2012视频“使用块、GCD和XPC进行异步设计模式”,第7节“分离控制和数据流”,大约在视频的第48分钟处讨论为什么会有问题,即如果用户快速滚动列表到第100个项目,那么其他99个请求都将排队等待。在极端情况下,您可以使用所有可用的工作线程。而且iOS只允许五个并发网络请求,因此没有必要使用所有这些线程(如果一些调度的块开始请求,但由于有超过五个正在进行,因此有些网络请求将开始失败)。

因此,在当前异步执行网络请求并使用缓存的方法之外,您还应该:

  1. 使用操作队列,这允许您(a)限制并发请求的数量; (b)打开取消操作的能力;

  2. 也要使用可取消的NSURLSession。您可以自己执行此操作,也可以使用类似于AFNetworking或SDWebImage的库。

  3. 当单元格被重复使用时,取消之前单元格的任何挂起请求(如果有)。

这是可以做到的,我们可以向您展示如何正确地执行,但这需要大量代码。最好的方法是使用许多具有缓存功能的UIImageView类别之一,但也处理所有这些其他问题。 SDWebImage UIImageView 类别非常出色。它极大地简化了您的代码:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *cellIdentifier = @"NewsCell";    // BTW, stay with your standard single cellIdentifier

    NewsCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier indexPath:indexPath];

    NewsItem *item = newsItemsArray[indexPath.row];

    cell.newsTitle.text = item.title;

    [cell.imageView sd_setImageWithURL:[NSURL URLWithString:item.image]
                      placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

    return cell;
}

1
非常好的解释,我也学到了正确的东西,你给出的答案非常好。实际上,我只是根据NSCache的问题给出了答案。但是,Sdwebimage和AFnetworking也非常适合完成这个任务。 - Nitin Gohel
@Rob,有没有更好的提问方式可以让我更好地理解你的答案?顺便感谢你的回答,我觉得非常有用。 - mcphersonjr
@Cedric,不,我认为你的问题很好。你问了“如何缓存”,并且提供了足够的上下文,使我们不会大喊“提供代码!”或“你尝试过什么?!”哈哈。只是假设应该使用GCD,在这种情况下并不是最优选择。这是苹果建议使用操作队列的情况之一。而那个UIImageView类别更进一步,让你远离基于NSOperation的代码。但是,如果在看完那个视频后,你还有什么不理解的地方,请告诉我们。 - Rob

10

可能你在NSCache中为每个图像设置相同的键值(Key)。

[cache setValue:[UIImage imageWithData:imageData] forKey:@"image"];

使用以下代码替代上面的代码,将 ForKey 设置为 Imagepath item.image,并使用 setObject 替代 setValue

[self.imageCache setObject:image forKey:item.image];

试试这个代码示例:

在.h类中:

@property (nonatomic, strong) NSCache *imageCache;

在 .m 类中:

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.imageCache = [[NSCache alloc] init];

    // the rest of your viewDidLoad
}

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

     static NSString *cellIdentifier = @"cell";
     NewsCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];

     NewsItem *item = [newsItemsArray objectAtIndex:indexPath.row];
     cell.newsTitle.text = item.title;

    UIImage *cachedImage =   [self.imageCache objectForKey:item.image];;
    if (cachedImage)
    {
        cell.imageView.image = cachedImage;
    }
    else
    {
        cell.imageView.image = [UIImage imageNamed:@"blankthumbnail.png"];

        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

               NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:item.image]];
               UIImage *image    = nil;
                if (imageData) 
                     image = [UIImage imageWithData:imageData];

                if (image)
                {

                     [self.imageCache setObject:image forKey:item.image];
                }
              dispatch_async(dispatch_get_main_queue(), ^{
                        UITableViewCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
                        if (updateCell)
                           cell.imageView.image = [UIImage imageWithData:imageData];
                           NSLog(@"Record String = %@",[cache objectForKey:@"image"]);
                  });
          });            
    }
    return cell;
}

如果在图像加载时重复使用单元格会发生什么?看起来你会在单元格中设置错误的图像。 - Kendall Helmstetter Gelner
是的,我也希望能够缓存objectAtIndex:indexPath.row,这样图片就属于缓存中相应的插槽和表格行。现在当你滚动时它会加载错误的图片到错误的行。我希望答案能反映出来,因为我正在使用可重用的单元格。顺便说一句,感谢你的回答。 - mcphersonjr
因为Nitin巧妙地在最后的dispatch_async中调用了cellForRowAtIndexPath,这确保了当您重用表格的另一行时,旧的已分派任务不会不适当地更新错误的行。因此,这个实现优雅地让旧请求完成,并为下一个单元格将新任务分派到后台。相当不错的实现(尽管在慢速连接或大型图像上,稍微更优雅的解决方案是取消旧请求或以某种方式推迟它们)。 - Rob
好的,我错过了cellForRow调用。虽然我同意你可能想在某个时候添加取消请求的功能。 - Kendall Helmstetter Gelner
@Nitin Gohel,谢谢你的回答。我无法按照你的答案对我的代码进行修改,从而得到令人满意的应用程序变化,这无疑是我的错误。然而,我将选择Rob的答案作为更好的解决方案和对我的问题的答案。 - mcphersonjr
@Nitin Gohel。保持缓存中数据的出色答案..+100 - Gajendra K Chauhan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接