这篇博客来源于今年的一个面试题,当咱们使用SDWebImgae框架中的sd_setImageWithURL: placeholderImage:方法在tableView或者collectionView里面下载图片的时候,滑动tableView发现它会优先下载展现在屏幕上的cell里面的图片,若是你不用SDWebImage框架如何实现?git
我iOS开发到如今大体是实习差很少一年,正式工做八九个月的样子,在此以前虽然常用诸如SDWebImgae、AFNetworking、MJRefresh、MJExtension等等第三方库,但却并未去研究过它们的源码,主要仍是时间问题吧,固然,如今我已经在研究它们的源码了,先学习、记录、仿写、再创造。github
当时,个人回答是,建立一个继承自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框架的缓存原理:app
实际上,就是在下载图片的时候,先在内存缓存中找是否存在缓存,不存在就去磁盘缓存中查找是否存在该图片(在沙盒里面,图片名通常是图片的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; }); }];
#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
#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等待显示图片的时间变长了。不放在全局队列里面,它本该只是等待下载图片的时间的,如今变成了下载图片的时间的+将数据写入沙盒的时间。
异步
// key:图片的url values: 相对应的operation对象 (判断该operation下载操做是否正在执行,当同一个url地址的图片正在下载,那么不须要再次下载,以避免重复下载,当下载操做执行完,须要移除) @property (nonatomic, strong) NSMutableDictionary *operations; // key:图片的url values: 相对应的图片 (缓存,当下载操做完成,须要将所下载的图片放到缓存中,以避免同一个url地址的图片重复下载) @property (nonatomic, strong) NSMutableDictionary *images;
当准备下载一张图片的时候,咱们是先查看下内存中是否存在这样的图片,也就是到images里面找下,若是没有,那么查看下磁盘缓存中是否有这样的图片,若是没有,看下这张图片是否正在被下载,若是仍是没有,就开始下载这张图片,代码:async
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; //加入字典,表示正在执行这次操做 } } }
@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