【问题标题】:What is the proper way to use NSCache with dispatch_async in a reusable table cell?在可重用的表格单元格中使用 NSCache 和 dispatch_async 的正确方法是什么?
【发布时间】:2013-10-20 00:20:56
【问题描述】:

我一直在寻找一种明确的方法来做到这一点,但没有找到任何可以举例说明并很好解释的地方。希望你能帮帮我。

这是我正在使用的代码:

- (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。

【问题讨论】:

  • 你可能想看看 SDWebImage 项目,这一切都已经弄清楚了。
  • 你能提供一个链接吗?

标签: iphone ios asynchronous nscache


【解决方案1】:

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

请参阅 WWDC 2012 视频 Asynchronous Design Patterns with Blocks, GCD, and XPC,第 7 节,“分离控制和数据流”,视频大约 48 分钟,讨论为什么会出现问题,即如果用户快速向下滚动列表到第 100 项,所有其他 99 个请求都将排队。在极端情况下,您可以使用所有可用的工作线程。而且iOS无论如何只允许五个并发的网络请求,所以用完所有这些线程是没有意义的(如果一些调度的块启动了由于超过五个而无法启动的请求,那么你的一些网络请求将开始失败)。

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

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

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

  3. 当一个单元被重用时,取消前一个单元的所有未决请求(如果有的话)。

这是可以做到的,我们可以向您展示如何正确地做到这一点,但代码量很大。最好的方法是使用许多 UIImageView 类别中的一种,它会进行缓存,但也可以处理所有这些其他问题。 SDWebImageUIImageView 类别相当不错。它极大地简化了您的代码:

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

【讨论】:

  • 非常好的解释,我也学到了正确的东西,因为它非常好的答案给你。实际上我只是根据 NSCache 的问题给出了答案。但是是的,Sdwebimage 以及 AFnetworking 也非常适合执行此任务
  • @Rob 有没有比我如何提问更好的方法来提问,这样可以更好地理解您的答案。顺便谢谢你的回答,我发现它很有用。
  • @Cedric 不,我认为你的问题很好。您问“我如何缓存”,并且您提供了足够的上下文以防止我们大喊“提供代码!”或“你试过什么?!”。哈哈。只是在这种情况下,应该使用 GCD 的假设并不是最优的。这是 Apple 建议使用操作队列的情况之一。而UIImageView 类别更进一步,让您远离基于NSOperation 的代码。但是,如果您在我的回答中有不明白的地方(尤其是在观看该视频后),请告诉我们。
【解决方案2】:

可能是你为NSCache的每张图片设置了相同的Key

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

使用 this 而不是上面将 ForKey 设置为 Imagepath item.image 并使用 setObject 而不是 setVlaue:-

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

【讨论】:

  • 如果在加载图像时重新使用单元格会发生什么?您似乎在单元格中设置了错误的图像。
  • 是的,我还希望缓存 objectAtIndex:indexPath.row 以便图像属于缓存中的相应插槽,就像表格行一样。现在,当您在加载时滚动时,它会将错误的图片加载到错误的行。我希望答案可以反映这一点,因为我使用的是可重复使用的细胞。顺便谢谢你的回答。
  • @KendallHelmstetterGelner 因为 Nitin 巧妙地在最后的 cellForRowAtIndexPath 中调用了 dispatch_async,它确保当您将单元格重用于表格的另一行时,旧的已调度任务不会更新错误的行不恰当。所以这个实现优雅地让旧请求完成,并为下一个单元格分派一个新任务到后台。相当不错的实现(尽管在慢速连接或大图像上,稍微更优雅的解决方案是取消旧请求或以某种方式推迟它们)。
  • 好点,我错过了 cellForRow 电话。虽然我同意您可能希望在某些时候添加取消请求。
  • @Nitin Gohel 谢谢你的回答。我无法获得对我的代码所做的更改(根据您的回答)导致令人满意的应用程序更改,这无疑是我的错。但是,我将选择 Rob 的答案作为对我问题的更好的解决方案和答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-01-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-11-19
  • 1970-01-01
  • 2022-01-25
相关资源
最近更新 更多