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。

推荐阅读更多精彩内容