【问题标题】:Download Images Asynchronously To NSData And Cache将图像异步下载到 NSData 和缓存
【发布时间】:2013-10-15 19:19:29
【问题描述】:

首先,这不是一个重复的问题。我已经阅读了很多关于 Stack Overflow 的问题,但它们并没有完全解决我的问题。

我正在从网络服务下载图像。由于没有人喜欢 UI 停滞不前,我使用线程单独下载图像。

NSURL *imageUrl = [NSURL URLWithString:storyImageURL];

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    thumbnailData = [NSData dataWithContentsOfURL:imageUrl];

    dispatch_async(dispatch_get_main_queue(), ^{
        thumbnail = [UIImage imageWithData:thumbnailData];
    });
});

如果我完全按照上面的方式使用代码,UI 将不会停止,直到它从 Web 服务获取数据但图像没有被缓存。

如果我不使用线程,那么 UI 将停止,但图像会使用 NSCoding 方法(存档)进行缓存。

我的问题是:如何同时使用线程和缓存缩略图?请不要推荐任何第三方库。

更新:一遍又一遍地查看代码后,我能想到两个问题:

1) 看起来 NSKeyedArchiver 和 NSKeyedUnarchiver 在线程完成下载图像之前被调用,但这只是一个猜测。在单独的存储文件中,我使用 NSKeyedArchiver 和 NSKeyedUnarchiver:

- (RSSChannel *)fetchRSSFeedWithCompletion:(void (^)(RSSChannel *, NSError *))block
{
    NSURL *url = [NSURL URLWithString:@"http://techcrunch.com/feed"];
    NSURLRequest *req = [NSURLRequest requestWithURL:url];

    RSSChannel *channel = [[RSSChannel alloc] init];
    TheConnection *connection = [[TheConnection alloc] initWithRequest:req];

    //[connection setCompletionBlock:block];

    NSString *cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    cachePath = [cachePath stringByAppendingPathComponent:@"HAHAHA.archive"];

    RSSChannel *cachedChannel = [NSKeyedUnarchiver unarchiveObjectWithFile:cachePath];

    if (!cachedChannel) 
        cachedChannel = [[RSSChannel alloc] init];

    RSSChannel *channelCopy = [cachedChannel copy];

    [connection setCompletionBlock:^(RSSChannel *obj, NSError *err) {
        if (!err) {

            [channelCopy addItemsFromChannel:obj];
            [NSKeyedArchiver archiveRootObject:channelCopy toFile:cachePath];
        }
        block(channelCopy, err);
    }];


    [connection setXmlRootObject:channel];
    [connection start];

    return cachedChannel;
}

2) 我能想到的第二个问题是他的 UI 在尝试将缩略图解码出缓存后没有刷新。

【问题讨论】:

    标签: ios objective-c nsdata grand-central-dispatch


    【解决方案1】:

    1) 看起来 NSKeyedArchiver 和 NSKeyedUnarchiver 在线程完成下载图像之前被调用,但这只是一个猜测。在单独的存储文件中,我使用 NSKeyedArchiver 和 NSKeyedUnarchiver:

    你在正确的轨道上。

    您需要在 RSSChannel 和远程获取数据的后台任务之间建立同步机制,以便在所有图像下载完成后才调用archiveRootObject

    处理此问题的一种方法是使用 dispatch_group 来处理所有图像下载。然后,您可以在执行archiveRootObject 之前让您的完成块等待该调度组。我前段时间为此写了一个要点,我认为它也应该对您有所帮助:https://gist.github.com/sdesimone/4579906。如果没有,请报告具体内容。 (可能您需要修复一些编译错误)。

    另一种处理方式是管理共享计数器:您

    1. 在提要解析开始时递增计数器并在完成块中递减:

      RSSChannel *channelCopy = [cachedChannel copy];
      
      INCREMENT_COUNTER
      

      [connection setCompletionBlock:^(RSSChannel *obj, NSError *err) { if (!err) {

          [channelCopy addItemsFromChannel:obj];
          DECREMENT_COUNTER;
       }
      block(channelCopy, err);
      }];
      
    2. 每次找到要下载的图像时增加计数器,然后在图像下载完成时减少计数器;当计数器达到零时,您就知道可以存档了:

      NSURL *imageUrl = [NSURL URLWithString:storyImageURL];
      
      INCREMENT_COUNTER;
      
      dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      
      thumbnailData = [NSData dataWithContentsOfURL:imageUrl];
      
      dispatch_async(dispatch_get_main_queue(), ^{
        thumbnail = [UIImage imageWithData:thumbnailData];
        DECREMENT_COUNTER;
        if (COUNTER_REACHED_ZERO)
          CALL_ARCHIVE_METHOD_ON_CHANNEL OBJECT
        });
      });
      

    这将需要一些重构:您需要将通道存储为一个属性(这样您就可以在原始方法之外使用它(参见第 1 点)。

    如何实现共享计数器的决定权交给您;只注意使其实现线程安全!

    希望这会有所帮助。

    【讨论】:

    • 我尝试了您在要点中提供的代码。在 RSSItem.m 下的 CDATA 块中,您添加了此代码 //-- ADD dispatch_group_async([self.parentParserDelegate imageDownloadGroup], dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{....... 我收到一个运行时错误“ARC 语义问题 - 选择器 'ImageDownloadGroup' 没有已知的实例方法”
    • 奇怪,它正在为我编译。试试:dispatch_group_async(((RSSChannel*)self.parentParserDelegate).imageDownloadGroup,...——只是一个选角问题……
    • 它的工作。非常感谢您提供的出色帮助,因为只有您才能理解问题背后的真正问题。我需要问的最后一件事。它的实现方式,你认为我需要转移到核心数据吗?因为涉及到很多图像。我计划在条目达到 100 后对其进行修剪。
    • 很高兴它成功了!关于核心数据的东西,是的,我认为您将需要它来获得良好的性能并更轻松地处理数据操作。
    • 顺便说一下图像以一种奇怪的方式加载。屏幕上的图像不显示。我必须上下滚动,然后显示所有这些。我应该在 StackOverflow 上打开另一个问题,还是它是块中某处同一问题的一部分?
    【解决方案2】:

    您是否将thumbnailData 保存为实例变量?在您发送之前,请检查是否设置了实例变量,如果是,则返回该值。如果没有,则运行您的调度块并将其保存为实例变量。

    【讨论】:

      【解决方案3】:

      假设您的目标是 iOS 5+,最自然的解决方案是酌情使用 NSURLCacheNSURLConnection +sendAsynchronousRequest:queue:completionHandler:。第三方解决方案通常会因为无知或希望支持 iOS 4 而忽略这些方法,因此您的选择要么有效地将这些东西的维护工作外包给 Apple,要么信任第三方,要么将自己的时间花在上面。

      例如

      NSURLRequest *request =
         [NSMutableURLRequest requestWithURL:imageURL];
      
      // just use the shared cache unless you have special requirements
      NSURLCache *cache = [NSURLCache sharedURLCache]; 
      
      NSCachedURLResponse *response = [cache cachedResponseForRequest:request];
      
      // we'll just lazily assume that if anything is in the
      // cache then it will do
      if(response)
      {
          [self proceedWithData:response.data];
      }
      else
      {
          // fetch the data
          [NSURLConnection
              sendAsynchronousRequest:request
              queue:[NSOperationQueue mainQueue] // this dictates where the completion
                                                 // handler is called; it doesn't make
                                                 // the fetch block the main queue
              completionHandler:
                  ^(NSURLResponse *response, NSData *data, NSError *error)
                  {
                       // TODO: error handling here
      
                       [cache
                           storeCachedResponse:
                               [[NSCachedURLResponse alloc]
                                   initWithResponse:response dat:data]
                           forRequest:request];
      
                        [self proceedWithData:data];
                  }];
      }
      

      NSURLCache 在 iOS 5 之前存在,但只是内存缓存。从 5 开始它也是一个磁盘缓存。

      【讨论】:

      • 有哪些例外,在哪里?它是临时写的,所以我可能在某个地方犯了一个愚蠢的错误。
      • +sendAsynchronousRequest:queue:completionHandler: 的问题是,如果您在例如 tableview 中使用它,则无法取消请求。如果用户快速滚动,您不想浪费时间完成对他们可能永远不会看到的图像的请求。
      • @jsd 你是完全正确的,显然,但它肯定比NSData +dataWithContentsOfURL: 更好,因为它不会阻塞。
      【解决方案4】:

      试试这个

          NSURL *imageUrl = [NSURL URLWithString:storyImageURL];
      
          UIButton *btnThumbnail = [[UIButton alloc] initWithFrame:CGRectMake(0, 10, 180, 280)];
          [self downloadingServerImageFromUrl:btnThumbnail AndUrl:imageUrl];
          [btnThumbnail addTarget:self action:@selector(onSelectEPaper:) forControlEvents:UIControlEventTouchUpInside];
          [self.view addSubview:viewPaperBg];
      
       - (void)onSelectEPaper:(id)sender
        {
        }
      
      
        -(void)downloadingServerImageFromUrl:(UIButton*)imgView AndUrl:(NSString*)strUrl
      {
      //    strUrl = [strUrl encodeUrl];
      //    strUrl = [strUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
      
      NSString* theFileName = [NSString stringWithFormat:@"%@.jpg",[[strUrl lastPathComponent] stringByDeletingPathExtension]];
      
      
      NSFileManager *fileManager =[NSFileManager defaultManager];
      NSString *fileName = [NSHomeDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"tmp/%@",theFileName]];
      
      
      imgView.backgroundColor = [UIColor darkGrayColor];
      UIActivityIndicatorView *actView = [[UIActivityIndicatorView alloc]initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite];
      [imgView addSubview:actView];
      [actView startAnimating];
      CGSize boundsSize = imgView.bounds.size;
      CGRect frameToCenter = actView.frame;
      // center horizontally
      if (frameToCenter.size.width < boundsSize.width)
          frameToCenter.origin.x = (boundsSize.width - frameToCenter.size.width) / 2;
      else
          frameToCenter.origin.x = 0;
      
      // center vertically
      if (frameToCenter.size.height < boundsSize.height)
          frameToCenter.origin.y = (boundsSize.height - frameToCenter.size.height) / 2;
      else
          frameToCenter.origin.y = 0;
      
      actView.frame = frameToCenter;
      
      
      dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
      dispatch_async(queue, ^{
      
          NSData *dataFromFile = nil;
          NSData *dataFromUrl = nil;
      
          dataFromFile = [fileManager contentsAtPath:fileName];
          //      NSLog(@"%@",fileName);
          if(dataFromFile==nil){
              //      NSLog(@"%@",strUrl);
              NSString *url =[strUrl stringByReplacingOccurrencesOfString:@"\n" withString:@""];
              url=[url stringByReplacingOccurrencesOfString:@"\t" withString:@""];
              url=[url stringByReplacingOccurrencesOfString:@" " withString:@""];
      
              url = [url stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
              //       dataFromUrl=[[[NSData alloc] initWithContentsOfURL:[NSURL URLWithString:url]] autorelease];
              //       dataFromUrl=[[NSData dataWithContentsOfURL:[NSURL URLWithString:url]] autorelease];
              NSError* error = nil;
              //     NSLog(@"%@", [NSURL URLWithString:url]);
              dataFromUrl = [NSData dataWithContentsOfURL:[NSURL URLWithString:url] options:NSDataReadingUncached error:&error];
      
              if (error) {
                  NSLog(@"%@", [error localizedDescription]);
              } else {
                  //    NSLog(@"Data has loaded successfully.");
              }
          }
      
          dispatch_sync(dispatch_get_main_queue(), ^{
      
              if(dataFromFile!=nil){
                  //    imgView.image = [UIImage imageWithData:dataFromFile];
                  [imgView setBackgroundImage:[UIImage imageWithData:dataFromFile] forState:UIControlStateNormal];
              }else if(dataFromUrl!=nil){
                  //    imgView.image = [UIImage imageWithData:dataFromUrl];
                  [imgView setBackgroundImage:[UIImage imageWithData:dataFromUrl] forState:UIControlStateNormal];
                  NSString *fileName = [NSHomeDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"tmp/%@",theFileName]];
      
                  BOOL filecreationSuccess = [fileManager createFileAtPath:fileName contents:dataFromUrl attributes:nil];
                  if(filecreationSuccess == NO){
                      //    NSLog(@"Failed to create the html file");
                  }
      
              }else{
                  //     imgView.image = [UIImage imageNamed:@"no_image.jpg"];
                  [imgView setBackgroundImage:[UIImage imageNamed:@"no_image.jpg"] forState:UIControlStateNormal];
              }
              [actView removeFromSuperview];
              //            [actView release];
              //    [imgView setBackgroundColor:[UIColor clearColor]];
          });
      });
      }
      

      【讨论】:

        猜你喜欢
        • 2017-12-24
        • 2011-07-01
        • 1970-01-01
        • 2012-12-05
        • 2013-02-10
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多