这篇博客来源于今年的一个面试题,当我们使用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

iOS:缓存与Operation优先级问题的更多相关文章

  1. iOS多线程自定义operation加载图片 不重复下载图片

    摘要:1:ios通过抽象类NSOperation封装了gcd,让ios的多线程变得更为简单易用:   2:耗时的操作交给子线程来完成,主线程负责ui的处理,提示用户的体验   2:自定义operati ...

  2. IOS缓存机制详解

    资料均来自互联网,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任. 人魔七七:http://www.cnblogs.com/qiqibo/ 为什么要有缓存 应用需要 ...

  3. 006 [翻译] Haneke(一个Swfit iOS缓存类)

    Github项目地址:https://github.com/Haneke/HanekeSwift Haneke是一个用swift写成的轻量级iOS类,以简单好用著称(design-decisions- ...

  4. ios 缓存相关信息收集

    链接:http://www.cnblogs.com/pengyingh/category/353093.html 使用NSURLCache让本地数据来代替远程UIWebView请求 摘要: 原文作者: ...

  5. iOS缓存框架-PINCache解读

    文/Amin706(简书作者)原文链接:http://www.jianshu.com/p/4df5aad0cbd4著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”. 在项目中总是需要缓存一 ...

  6. 【转】IOS缓存机制详解

    人魔七七:http://www.cnblogs.com/qiqibo/ 为什么要有缓存 应用需要离线工作的主要原因就是改善应用所表现出的性能.将应用内容缓存起来就可以支持离线.我们可以用两种不同的缓存 ...

  7. iOS缓存

    存储缓存: 第三方应用只能把信息保存在应用程序的沙盒中.因为缓存数据不是用户产生的,所以它应该被保存在NSCachesDirectory,而不是NSDocumentsDirectory.为缓存数据创建 ...

  8. iOS开发The Operation couldn't be completed.(LaunchServicesError error 0.)的解决方法

    显示错误:The Operation couldn't be completed.(LaunchServicesError error 0.)解决办法:第1种方法.点击当前的模拟器,点击IOS Sim ...

  9. IOS缓存管理之PINCache使用

    前言: 今年重点在于公司iOS架构的梳理工作,上周整理了http请求接口管理与解耦,接下来准备整理一下项目中的缓存处理,目前项目中使用的是PINCache,去年加入这个开源框架时并没有对这个框架进行了 ...

随机推荐

  1. C++类库介绍

    如果你有一定的C基础可能学起来比较容易些,但是学习C++的过程中又要尽量避免去使用一些C中的思想:平时还要多看一些高手写的代码,遇到问题多多思考,怎样才能把问题抽象化,以使自己头脑中有类的概念:最后别 ...

  2. 从快的线上callback hell代码说起

    概述 就像谈到闭包必须要说js变量作用域一样,谈到 promise 之前肯定要先说谈异步编程.一直以来, javascript 处理异步方式都是使用 callback 方式,对与写 javascrip ...

  3. [ucgui] 对话框6——触屏位置简单例子

    >_<:直接调用函数获得触屏位置: xPhys = GUI_TOUCH_GetxPhys(); /* Get the A/D mesurement result in x */ yPhys ...

  4. [WinAPI] API 4 [注册][创建][消息][第一个框架类窗口]

    首先注册了窗口类,然后创建了一个窗口,创建窗口时指定的窗口的属性和窗口消息的处理函数.函数消息的处理函数大多调用系统默认函数来处理. #include<windows.h> /*全局变量* ...

  5. Spring依赖注入(IOC)那些事

    小菜使用Spring有几个月了,但是对于它的内部原理,却是一头雾水,这次借着工作中遇到的一个小问题,来总结一下Spring. Spring依赖注入的思想,就是把对象交由Spring容器管理,使用者只需 ...

  6. C#与数据库访问技术总结(十四)之DataAdapter对象

    DataAdapter对象 DataAdapter对象主要用来承接Connection和DataSet对象. DataSet对象只关心访问操作数据,而不关心自身包含的数据信息来自哪个Connectio ...

  7. 单线程&浏览器多线程

    知乎答案:http://www.zhihu.com/question/31982417/answer/54136684 copy大牛的好文:from http://www.cnblogs.com/Ma ...

  8. 地理围栏算法解析(Geo-fencing)

    地理围栏算法解析 http://www.cnblogs.com/LBSer/p/4471742.html 地理围栏(Geo-fencing)是LBS的一种应用,就是用一个虚拟的栅栏围出一个虚拟地理边界 ...

  9. paip.powerdesign cdm pdm文件 代码生成器 java web 页面 实现

    paip.powerdesign cdm pdm文件 代码生成器 java web 页面 实现 准备从pd cdm生成java web 页面...但是,ms无直接地生成软件.... 只好自己解析cdm ...

  10. paip.字符串操作uapi java php python总结..

    paip.字符串操作uapi java php python总结.. java and php 相互转换.. import strUtil>>>  requiry(strUtil.p ...