这篇博客来源于今年的一个面试题,当我们使用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. 读《你必须知道的.NET》继承本质论 Bird bird=new Chicken()

    我们创建如下的三层继承层次类. public abstract class Animal { public abstract void ShowType(); } public class Bird ...

  2. [计算机图形学] OpenGL读取obj文件并显示其3D效果

     读取三维网格模型(Wavefront OBJ文件) 无法向立方体:cube.obj 有法向兔子模型:bunny.obj 有法向有纹理八字模型:Eight.obj OBJ文件的格式可参考:http: ...

  3. [BTS] BizTalk EDI AS2 Error 1

    A message sent to adapter "HTTP" on send port "XZ.Test.AS2" with URI "https ...

  4. sql 取首次投资的人

    --- 11月 ---首次投资笔数和投资金额 ) AS stNum,sum(amount) AS stAmount FROM ( ),createtime,) AS riqi,a.amount,a.u ...

  5. 网站标题ico那些事

    浏览器打开一个网页都会有一个标题,用来显示当前页面的相关内容,如网站名称或者一篇文章的大标题,而定义它应该显示啥的话完全由HTML中title标签的内容决定. 如我们的大博客园:

  6. paip.语义分析--单字词形容词表180个

    paip.语义分析--单字词形容词表180个  INSERT INTO t (word)  SELECT DISTINCT word FROM `word_main` where tsisin is ...

  7. atitit.重装系统需要备份的资料总结 o84..

    atitit.重装系统需要备份的资料总结 o84.. 这里我的系统装在C盘..所以需要备份C盘的东西就好了.. 1.DESKTOP,这个目录要备份.如果重要资料 2.docume nt,这个需要..W ...

  8. hdu4508 完全背包,湫湫系列故事——减肥记I

    湫湫系列故事——减肥记I 对于01背包和完全背包,昨晚快睡着的时候,突然就来了灵感 区别:dp[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值 在第二重循环,01 是倒着循环的,因 ...

  9. JQ中mouseover和mouseenter的区别

    我最近也在学习JQuery,所以最近对JQ中的一些小问题进行总结,方便学习. 在对于刚开始学习JQ的初学者来说,mouseover事件和mouseenter事件总是傻傻分不清楚,毕竟刚开始学习的时候, ...

  10. android: 服务的生命周期

    9.4   服务的生命周期 之前章节我们学习过了活动以及碎片的生命周期.类似地,服务也有自己的生命周期, 前面我们使用到的 onCreate().onStartCommand().onBind()和 ...