LazyTableImages解析(图片懒加载)

简单了解

  • 懒加载:
    顾名思义,用到的时候才去加载,又称延时加载。OC中常用两种懒加载如下:
    1. 非image实例懒加载:
    - (UILabel *) nameLabel {
    //手动实现实例的get方法,调用这个实例的时候判断该实例是否已初始化,若未初始化则先初始化后返回
      if (!_nameLabel) {
          _nameLabel = [[UILabel alloc] init];
      }
      return _nameLabel;
    }
    
    1. image实例懒加载,多用于tableView加载图片中,实现逻辑同上,用到的时候再加载。核心思想:
      当tablew开始滑动的时候停止请求图片,当tableView停址滑动的时候开始请求图片。如果在请求过程中滑动tableView,则手动停止请求。
  • 为什么使用懒加载
    当一个项目做完所有功能,并且测试通过后,身为程序猿应该干什么?坐等下一个需求?错!!!
    这个时候表面上功能点全部跑通,但是潜在问题还是存在的。比如滑动tableView的时候偶现卡顿,如果用户很大,这就是硬伤!一般情况下,创业公司的源代码中,异步发送请求的同时并未做其他操作,可以滑动tableView,这个时候可以看到tableView的fps很低,就是因为滑动的同事在请求数据,虽然异步,但在渲染的时候就会影响性能。
    所以这就是为什么!
  • 隆重的介绍本文主角LazyTableImages
    LazyTableImages是苹果官方Demo,用来展示图片懒加载。通过源代码,可以很清晰的看出图片懒加载的思想。这里是LazyTableImages Demo链接,下文将分析核心代码片段。

核心代码解析

  • 单个加载Cell
    if (!appRecord.appIcon) {
      if (self.tableView.dragging == NO && self.tableView.decelerating == NO) {
          [self startIconDownload:appRecord forIndexPath:indexPath];
      }
      // if a download is deferred or in progress, return a placeholder image
      cell.imageView.image = [UIImage imageNamed:@"Placeholder.png"];                
    } else {
      cell.imageView.image = appRecord.appIcon;
    }
    
    这里dragging表示tableView停止拖动,decelerating表示tableView加速度为0,当tableView停止拖动,并且加速度为0时调用startIconDownload:forIndexPath:indexPath:方法请求图片并加载;否则,加载暂位图。
    注意:if(!appRecord.appIcon),这里的处理方式是cell的Icon如果没值再去请求。
  • 加载当前屏幕所显示的cell
    - (void)loadImagesForOnscreenRows
    {
      if (self.entries.count > 0)
      {
          NSArray *visiblePaths = [self.tableView indexPathsForVisibleRows];
          for (NSIndexPath *indexPath in visiblePaths)
          {
              AppRecord *appRecord = (self.entries)[indexPath.row];
              
              if (!appRecord.appIcon)
              // Avoid the app icon download if the app already has an icon
              {
                  [self startIconDownload:appRecord forIndexPath:indexPath];
              }
          }
      }
    }
    
    
    #pragma mark - UIScrollViewDelegate
    
    // -------------------------------------------------------------------------------
    //    scrollViewDidEndDragging:willDecelerate:
    //  Load images for all onscreen rows when scrolling is finished.
    // -------------------------------------------------------------------------------
    - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
    {
      if (!decelerate)
      {
          [self loadImagesForOnscreenRows];
      }
    }
    
    // -------------------------------------------------------------------------------
    //    scrollViewDidEndDecelerating:scrollView
    //  When scrolling stops, proceed to load the app icons that are on screen.
    // -------------------------------------------------------------------------------
    - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
    {
        [self loadImagesForOnscreenRows];
    }
    
    这段代码中用到了scrollView的代理方法scrollViewDidEndDraggingscrollViewDidEndDecelerating,通过[self.tableView indexPathsForVisibleRows]获取当前显示在屏幕上cell的indexPaths数组,然后一个for循环调用startIconDownload:forIndexPath:indexPath:依次请求图片。由此可见,*startIconDownload:forIndexPath:indexPath:才是本文的关键所在。
  • startIconDownload:forIndexPath:indexPath:
    - (void)startIconDownload:(AppRecord *)appRecord forIndexPath:(NSIndexPath *)indexPath
    {
      IconDownloader *iconDownloader = (self.imageDownloadsInProgress)[indexPath];
      if (iconDownloader == nil) 
      {
          iconDownloader = [[IconDownloader alloc] init];
          iconDownloader.appRecord = appRecord;
          [iconDownloader setCompletionHandler:^{
              
              UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
              
              // Display the newly loaded image
              cell.imageView.image = appRecord.appIcon;
              
              // Remove the IconDownloader from the in progress list.
              // This will result in it being deallocated.
              [self.imageDownloadsInProgress removeObjectForKey:indexPath];
              
          }];
          (self.imageDownloadsInProgress)[indexPath] = iconDownloader;
          [iconDownloader startDownload];  
      }
    }
    
    看了两遍才读懂这里的意思,从写代码就可以看出一个人的内功,所以一定要多读源码。
    1. self.imageDownloadsInProgress是一个NSMutableDictionary类型的实例,主要用来缓存IconDownloader的实例IconDownloader是封装好的一个网络请求类,稍候会介绍。
    2. 现在来说说为什么要缓存IconDownloader的实例,上文已经介绍了懒加载的思想了,如果在请求过程中滑动tableView,则手动停止请求。想一想,手动停止请求就证明图片并未请求成功,但是这个请求类已经创建了,为了下次tableView停止滑动后,不再重新创建要缓存IconDownloader的实例。
    3. 这里还有一点需要注意,这句代码 [self.imageDownloadsInProgress removeObjectForKey:indexPath],为什么请求成功后还要删除这个实例,往上翻看注意if(!appRecord.appIcon)的时候才进行请求,当请求成功后,Icon已经存在了,就可以直接加载,所以IconDownloader的实例就没必要继续缓存了。
  • IconDownloader类
    // -------------------------------------------------------------------------------
    //    startDownload
    // -------------------------------------------------------------------------------
    - (void)startDownload
    {
      NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:self.appRecord.imageURLString]];
    
      // create an session data task to obtain and download the app icon
      _sessionTask = [[NSURLSession sharedSession] dataTaskWithRequest:request
                                                     completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
          
          // in case we want to know the response status code
          //NSInteger HTTPStatusCode = [(NSHTTPURLResponse *)response statusCode];
    
          if (error != nil)
          {
              if ([error code] == NSURLErrorAppTransportSecurityRequiresSecureConnection)
              {
                  // if you get error NSURLErrorAppTransportSecurityRequiresSecureConnection (-1022),
                  // then your Info.plist has not been properly configured to match the target server.
                  //
                  abort();
              }
          }
                                                         
          [[NSOperationQueue mainQueue] addOperationWithBlock: ^{
              
              // Set appIcon and clear temporary data/image
              UIImage *image = [[UIImage alloc] initWithData:data];
              
              if (image.size.width != kAppIconSize || image.size.height != kAppIconSize)
              {
                  CGSize itemSize = CGSizeMake(kAppIconSize, kAppIconSize);
                  UIGraphicsBeginImageContextWithOptions(itemSize, NO, 0.0f);
                  CGRect imageRect = CGRectMake(0.0, 0.0, itemSize.width, itemSize.height);
                  [image drawInRect:imageRect];
                  self.appRecord.appIcon = UIGraphicsGetImageFromCurrentImageContext();
                  UIGraphicsEndImageContext();
              }
              else
              {
                  self.appRecord.appIcon = image;
              }
              
              // call our completion handler to tell our client that our icon is ready for display
              if (self.completionHandler != nil)
              {
                  self.completionHandler();
              }
          }];
      }];
      
      [self.sessionTask resume];
    }
    
    // -------------------------------------------------------------------------------
    //    cancelDownload
    // -------------------------------------------------------------------------------
    - (void)cancelDownload
    {
      [self.sessionTask cancel];
      _sessionTask = nil;
    }
    
    这段代码应该能看懂,子线程中请求数据,主线程中刷新UI。唯一需要介绍的是这里请求到的图片并不是直接赋值给imageView的,烦请各位看官上翻查看请求成功后的处理方法,图片请求成功后,是调用Graphics方法重新画了一张位图。相信很多人会有疑问,为什么要重新画,而不直接复制,这里简单的介绍一下:

    iOS加载的图片都是位图,JPEG和PNG,位图就是像素的集合。
    在不同的显示屏上一个像素的字节大小不同,一张图片在渲染到屏幕上必经的就是解压缩过程,因为系统需要知道图片像素的详细信息,才能进行渲染。所以,未经过解压缩的图片都要经过系统的强制解压缩才能成功显示在屏幕上,这个工程我们看不到,但是必经。
    什么时候需要解压缩?当需要加载大量图片的时候,为了不影响性能,就需要我们手动"解压缩",调用系统API画一张。附一篇关于解压缩的大牛博客谈谈 iOS 中图片的解压缩

总结

这个Demo很多源代码都值得我们学习,大家可以下载源码查看,遇到不懂的地方一定要多看文档。这个Demo是调用系统API进行网络请求,在实际开发过程中大多数情况是用到三方封装好的网络库,相信大家也能够根据实际情况进行参考。有必要的话,我会写一个Demo。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,602评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,442评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,878评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,306评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,330评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,071评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,382评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,006评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,512评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,965评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,094评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,732评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,283评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,286评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,512评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,536评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,828评论 2 345

推荐阅读更多精彩内容