zoukankan      html  css  js  c++  java
  • iOS:缓存与Operation优先级问题

    这篇博客来源于今年的一个面试题,当我们使用SDWebImgae框架中的sd_setImageWithURL: placeholderImage:方法在tableView或者collectionView里面下载图片的时候,滑动tableView发现它会优先下载展示在屏幕上的cell里面的图片,如果你不用SDWebImage框架如何实现?

    我iOS开发到现在大致是实习差不多一年,正式工作八九个月的样子,在此之前虽然经常使用诸如SDWebImgae、AFNetworking、MJRefresh、MJExtension等等第三方库,但却并未去研究过它们的源码,主要还是时间问题吧,当然,现在我已经在研究它们的源码了,先学习、记录、仿写、再创造。

     

    当时,我的回答是,创建一个继承自NSOperation的ZYOperation类来下载图片,将相应的Operation放到OperationQueue中,监听tableView的滚动,当发现cell不在屏幕时,将之对应的operation对象暂停掉,当它再出现在屏幕上时,再让它下载。

    严格来说,我这只能算是提供了一种解决方案,事实上,NSOperation对象只能取消(cancel),而不能暂停(pause)。

    SDWebImage内部使用GCD实现的,调整GCD的优先级即可:

    #define DISPATCH_QUEUE_PRIORITY_HIGH 2
    #define DISPATCH_QUEUE_PRIORITY_DEFAULT 0
    #define DISPATCH_QUEUE_PRIORITY_LOW (-2)
    #define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
    

     所以,在我实际操作中,发现也只是需要调整operation的优先级即可。在此基础上,我还实现了图片缓存策略,参考SDWebImage框架的缓存原理:

    实际上,就是在下载图片的时候,先在内存缓存中找是否存在缓存,不存在就去磁盘缓存中查找是否存在该图片(在沙盒里面,图片名一般是图片的url,因为要确保图片名唯一)。如果沙盒中有改图片缓存,就读取到内存中,如果不存在,再进行下载图片的操作。使用SDWebImage的流程代码如下:

    [[SDWebImageDownloader sharedDownloader] downloadImageWithURL:[NSURL URLWithString:@"http://images2015.cnblogs.com/blog/471463/201511/471463-20151105102529164-1764637824.png"] options:SDWebImageDownloaderUseNSURLCache progress:^(NSInteger receivedSize, NSInteger expectedSize) {
            
        } completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
            
            SDImageCache *cache = [SDImageCache sharedImageCache];
            [cache storeImage:image forKey:@"http://images2015.cnblogs.com/blog/471463/201511/471463-20151105102529164-1764637824.png"];
            //从内存缓存中取出图片
            UIImage *imageOne = [cache imageFromMemoryCacheForKey:@"http://images2015.cnblogs.com/blog/471463/201511/471463-20151105102529164-1764637824.png"];
            //从磁盘缓存中取出图片
            UIImage *imageTwo = [cache imageFromDiskCacheForKey:@"http://images2015.cnblogs.com/blog/471463/201511/471463-20151105102529164-1764637824.png"];
            
            
            NSLog(@"%@   %@", imageOne, imageTwo);
            
            dispatch_async(dispatch_get_main_queue(), ^{
                self.iconView.image = image;
            });
        }];
    

     

    1. 图片缓存与沙河目录
      沙盒中一般是存在三个文件夹,Document,Library,tmp。
      tmp:临时文件存储的地方,如果将一个文件存储在此目录下,这个文件何时会被删除是不可预知的,也就是说,随时会被删除。
      Document:保存在此目录下的文件默认是会被同步到iCloud
      Library:不会被同步到iCloud,同时在不主动删除的情况下可以长时间存在

      一般来说,对与这样的一些非关键的图片,我会保存在Library的cache目录下。一般都有一个获取各个文件目录的工具类,也可以写成单例,代码:
      #import <Foundation/Foundation.h>
      
      typedef enum {
          ZYFileToolTypeDocument,
          ZYFileToolTypeCache,
          ZYFileToolTypeLibrary,
          ZYFileToolTypeTmp
      } ZYFileToolType;
      
      @interface ZYFileTool : NSObject
      /**  获取Document路径  */
      + (NSString *)getDocumentPath;
      /**  获取Cache路径  */
      + (NSString *)getCachePath;
      /**  获取Library路径  */
      + (NSString *)getLibraryPath;
      /**  获取Tmp路径  */
      + (NSString *)getTmpPath;
      /**  此路径下是否有此文件存在  */
      + (BOOL)fileIsExists:(NSString *)path;
      
      /**
       *  创建目录下文件
       *  一般来说,文件要么放在Document,要么放在Labrary下的Cache里面
       *  这里也是只提供这两种存放路径
       *
       *  @param fileName 文件名
       *  @param type     路径类型
       *  @param context  数据内容
       *
       *  @return 文件路径
       */
      + (NSString *)createFileName:(NSString *)fileName  type:(ZYFileToolType)type context:(NSData *)context;
      
      /**
       *  读取一个文件
       *
       */
      + (NSData *)readDataWithFileName:(NSString *)fileName type:(ZYFileToolType)type;
      @end
      
      
      
      #import "ZYFileTool.h"
      
      @implementation ZYFileTool
      
      + (NSString *)getRootPath:(ZYFileToolType)type
      {
          switch (type) {
              case ZYFileToolTypeDocument:
                  return [self getDocumentPath];
                  break;
              case ZYFileToolTypeCache:
                  return [self getCachePath];
                  break;
              case ZYFileToolTypeLibrary:
                  return [self getLibraryPath];
                  break;
              case ZYFileToolTypeTmp:
                  return [self getTmpPath];
                  break;
              default:
                  break;
          }
          return nil;
      }
      
      + (NSString *)getDocumentPath
      {
          return [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
          
      }
      
      + (NSString *)getCachePath
      {
          return [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
      }
      
      + (NSString *)getLibraryPath
      {
          return [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject];
      }
      
      + (NSString *)getTmpPath
      {
          return NSTemporaryDirectory();
      }
      
      + (BOOL)fileIsExists:(NSString *)path
      {
          if (path == nil || path.length == 0) {
              return false;
          }
          return [[NSFileManager defaultManager] fileExistsAtPath:path];
      }
      
      
      + (NSString *)createFileName:(NSString *)fileName  type:(ZYFileToolType)type context:(NSData *)context
      {
          if (fileName == nil || fileName.length == 0) {
              return nil;
          }
          fileName = [fileName stringByReplacingOccurrencesOfString:@"/" withString:@"-"];
          NSString *path = [[self getRootPath:type] stringByAppendingPathComponent:fileName];
          if (![self fileIsExists:path])
          {
      //        if (![[NSFileManager defaultManager] removeItemAtPath:path error:nil]) {
      //            return nil;
      //        }
              [[NSFileManager defaultManager] createFileAtPath:path contents:context attributes:nil];
          }
          
          return path;
      }
      
      + (NSData *)readDataWithFileName:(NSString *)fileName type:(ZYFileToolType)type
      {
          if (fileName == nil || fileName.length == 0) {
              return nil;
          }
      
          fileName = [fileName stringByReplacingOccurrencesOfString:@"/" withString:@"-"];
          NSString *path = [[self getRootPath:type] stringByAppendingPathComponent:fileName];
          
          if ([self fileIsExists:path])
          {
              return [[NSFileManager defaultManager] contentsAtPath:path];
          }
          return nil;
      }
      
      @end
      
    2. 防止图片被重复下载

      这个问题面试经常被问到吧,要防止图片被重复下载的话,如果实在内存缓存中,设置一个Dictionary使得它的key为图片的url,value为对应图片(即UIImage),当然,仅仅这样是不够的,如果图片正在被下载,相应的key-value并没有被设置,这个时候,就会重新下载图片。

      在本例子中,我使用的是NSOperation下载图片,那么可以还可以设置一个Dictionary,使得它的key为图片url,value为对应图片的下载操作(即operation对象)。这样的话,当把一个operation加入operationQueue的时候,你就将对应的key-value加入字典,当operation对象下载完图片的时候,你就将这个字典对应的key-value移除。


    3. 自定义NSOperation

      自定义NSOperation主要是重写它的main方法,将耗时操作放进去。这里需要对应cell的indexPath,这样才能在图片下载完成之后找到对应的cell更新UIImageView,同样也需要图片的url,这样才能在图片下载完成之后,将对应字典里面的url-operation键值对移除掉等。

      相应代码:
      #import <Foundation/Foundation.h>
      #import <UIKit/UIKit.h>
      @class ZYDownLoadImageOperation;
      
      @protocol ZYDownLoadImageOperationDelegate <NSObject>
      @optional
      - (void)DownLoadImageOperation:(ZYDownLoadImageOperation *)operation didFinishDownLoadImage:(UIImage *)image;
      @end
      @interface ZYDownLoadImageOperation : NSOperation
      @property (nonatomic, weak) id<ZYDownLoadImageOperationDelegate> delegate;
      @property (nonatomic, copy) NSString *url;
      @property (nonatomic, strong) NSIndexPath *indexPath;
      @end
      
      
      
      #import "ZYDownLoadImageOperation.h"
      #import "ZYFileTool.h"
      
      @implementation ZYDownLoadImageOperation
      - (void)main   //重写main方法即可
      {
          @autoreleasepool
          {    //在子线程中,并不会自动添加自动释放池,所以,手动添加,免得出现内存泄露的问题
              NSURL *DownLoadUrl = [NSURL URLWithString:self.url];
              if (self.isCancelled) return;          //如果下载操作被取消,那么就无需下面操作了
              NSData *data = [NSData dataWithContentsOfURL:DownLoadUrl];
              if (self.isCancelled) return;
              UIImage *image = [UIImage imageWithData:data];
              if (self.isCancelled) return;
              
              dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                  [ZYFileTool createFileName:self.url type:ZYFileToolTypeCache context:data]; //将数据缓存到本地
              });
              
              
              if ([self.delegate respondsToSelector:@selector(DownLoadImageOperation:didFinishDownLoadImage:)]) {
                  dispatch_async(dispatch_get_main_queue(), ^{   //回到主线程,更新UI
                      
                      [self.delegate DownLoadImageOperation:self didFinishDownLoadImage:image];
                  });
              }
          }
      }
      @end
      

      我把将数据写入沙盒操作放到了全局队列里面,在编码的时候,请时刻注意I/O的操作不应该阻塞CPU操作的。因为I/O操作,一般来说都会比较耗时,就iOS开发来说,如果把这类操作放到主线程中执行,就会引起界面迟钝、卡顿等现象出现。
      当然,就这里来说,即使不放在全局队列里面也不会引起界面迟钝等现象,因为operation操作本身就是在一个子线程里面,但是会引起回调往后延迟,也就是说,UIImageView等待显示图片的时间变长了。不放在全局队列里面,它本该只是等待下载图片的时间的,现在变成了下载图片的时间的+将数据写入沙盒的时间。

    4. 缓存思路

      首先,先要有这样两个字典,上面提到了的:
      //  key:图片的url  values: 相对应的operation对象  (判断该operation下载操作是否正在执行,当同一个url地址的图片正在下载,那么不需要再次下载,以免重复下载,当下载操作执行完,需要移除)
      @property (nonatomic, strong) NSMutableDictionary *operations;
      
      //  key:图片的url  values: 相对应的图片        (缓存,当下载操作完成,需要将所下载的图片放到缓存中,以免同一个url地址的图片重复下载)
      @property (nonatomic, strong) NSMutableDictionary *images;
      

      当准备下载一张图片的时候,我们是先查看下内存中是否存在这样的图片,也就是到images里面找下,如果没有,那么查看下磁盘缓存中是否有这样的图片,如果没有,看下这张图片是否正在被下载,如果还是没有,就开始下载这张图片,代码:

      UIImage *image = self.images[app.icon];   //优先从内存缓存中读取图片
          
          if (image)     //如果内存缓存中有
          {
              cell.imageView.image = image;
          }
          else
          {
              //如果内存缓存中没有,那么从本地缓存中读取
              NSData *imageData = [ZYFileTool readDataWithFileName:app.icon type:ZYFileToolTypeCache];
              
              if (imageData)  //如果本地缓存中有图片,则直接读取,更新
              {
                  UIImage *image = [UIImage imageWithData:imageData];
                  self.images[app.icon] = image;
                  cell.imageView.image = image;
              }
              else
              {
                  cell.imageView.image = [UIImage imageNamed:@"TestMam"];
                  ZYDownLoadImageOperation *operation = self.operations[app.icon];
                  if (operation)
                  {  //正在下载(可以在里面取消下载)
                  }
                  else
                  { //没有在下载
                      operation = [[ZYDownLoadImageOperation alloc] init];
                      operation.delegate = self;
                      operation.url = app.icon;
                      operation.indexPath = indexPath;
                      operation.queuePriority = NSOperationQueuePriorityNormal;
                      [self.queue addOperation:operation];  //异步下载
                      
                      
                      self.operations[app.icon] = operation;  //加入字典,表示正在执行此次操作
                  }
              }
          }
      
    5. 优先级问题

      NSOperation有个queuePriority属性:
      @property NSOperationQueuePriority queuePriority;
      
      typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
      	NSOperationQueuePriorityVeryLow = -8L,
      	NSOperationQueuePriorityLow = -4L,
      	NSOperationQueuePriorityNormal = 0,
      	NSOperationQueuePriorityHigh = 4,
      	NSOperationQueuePriorityVeryHigh = 8
      };
      

      allow,init创建出来的operation在没有设置的情况下,queuePriority是NSOperationQueuePriorityNormal。在这个例子中,我是监听scrollView的滚动,然后拿到所以的operation设置它们的优先级为normal,在利用tableView的indexPathsForVisibleRows方法,拿到所以展示在屏幕上的cell,将它们对应的operation设置为VeryHigh,相应代码:

      - (void)scrollViewDidScroll:(UIScrollView *)scrollView  //设置优先级别,效果是,最先下载展示在屏幕上的图片(本例子中图片太小了,没有明显的效果出现,可以设置更多的一些高清大图)
      {
          dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
          
          dispatch_apply(self.apps.count, queue, ^(size_t i) {
              ZYApp *appTmp = self.apps[i];
              NSString *urlStr = appTmp.icon;
      
              ZYDownLoadImageOperation *operation = self.operations[urlStr];
              if (operation)
              {
                  operation.queuePriority = NSOperationQueuePriorityNormal;
              }
          });
          
          NSArray *tempArray = [self.tableView indexPathsForVisibleRows];
          
          dispatch_apply(tempArray.count, queue, ^(size_t i) {
              NSIndexPath *indexPath = tempArray[i];
      
              ZYApp *appTmp = self.apps[indexPath.row];
              NSString *urlStr = appTmp.icon;
              ZYDownLoadImageOperation *operation = self.operations[urlStr];
              if (operation)
              {
                  operation.queuePriority = NSOperationQueuePriorityVeryHigh;
              }
          });
          
      }
      

       首先要说明的是,如果你想看到很明显的效果,那么需要将图片换下,换成大的、高清点的图片,图片数量越多效果会越好。建议在真机下调试,或者将operationQueue的maxConcurrentOperationCount改成1,真机调试,是有效果的,我这里是设置为3的。

      基本思路已经说完了,就是动态改变优先级。

      代码里面有个dispatch_apply,其实就是我们常用的for循环的异步版本。这么说吧,平时的for一般是放在主线程里面调用,是的i是一次增加,是从0,再到1,再到2等等。而是用dispatch_apply可以使得不再是同步依次增加,而是可以并发的一定范围内的随机值。这样可以充分利用iPhone的多核处理器,更加快速的处理一些业务。

      不过,需要注意的是,这里由于是并发的执行,所以是在子线程里面,并且后面的值不依赖前面的任何值,否则这么用就会出现问题。更加详细的资料请查询文档。 

     

     Github地址:https://github.com/wzpziyi1/CustomOperation

    如果对您有帮助,请帮忙点击下Star

     

     

  • 相关阅读:
    HTML DOM教程 14HTML DOM Document 对象
    HTML DOM教程 19HTML DOM Button 对象
    HTML DOM教程 22HTML DOM Form 对象
    HTML DOM教程 16HTML DOM Area 对象
    ubuntu 11.04 问题 小结
    VC6.0的 错误解决办法 小结
    boot.img的解包与打包
    shell里 截取字符串
    从零 使用vc
    Imagemagick 对图片 大小 和 格式的 调整
  • 原文地址:https://www.cnblogs.com/ziyi--caolu/p/5446370.html
Copyright © 2011-2022 走看看