下载的话,我查阅了不少人写的,像SDWebImage,使用的是NSURLConnection,可是我这里准备使用NSURLsession,使用NSURLSessionDataDelegate的代理方法实现下载数据.缓存
说点题外话:我为何选择NSURLsession二部选择NSURLConnection。由于iOS9以前在作网络链接的时候,咱们使用的时NSURLConnection,可是iOS9以后NSURLConnection宣布被弃用了,在2013年的WWDC大会上,苹果就已经设计出NSURLConnection的继任者NSURLSession,他使用起来比NSURLConnection更加简单,更增强大。安全
在这个过程中,还会用到GCD与NSOperation来管理下载线程,为何混合使用呢?咱们使用子类化NSOperation来高复抽象咱们的下载线程进行抽象化,这样使咱们的下载模块更加清晰,在整个不算太复杂的下载过程当中,让接口变得简单。GDC咱们在下载中局部会使用到,GCD的优势咱们都知道,简单,易用,节省代码,使用block让代码变得更加简洁。网络
基本上使用的东西上面都总结完了,开始进入下载的设计。session
使用子类化自定义NSOperation,这样一个下载就是一条线程,管理这些线程的话,就须要一个下载管理器,咱们就是先来构建这个下载管理器。app
这个管理器在整个下载模块中起到的就是对线程资源进行管理,起到一个工具的做用,这样的话咱们须要把管理器构建成一个单例类,因此这里咱们须要先使用单例模式来达到数据共享的目的。异步
+(instancetype)shareDownloader{ static LYImageDownloader *lyImageDownloader; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ lyImageDownloader = [[LYImageDownloader alloc] init]; }); return lyImageDownloader; }
以上就是咱们下载管理器的单例。async
整个下载的时候,经过阅读开源库,查找资料,发现不少的设计者他们的下载都具有状态监听,这个状态指的就是像下载进度,完成进度,错误信息回调。这些都是下载过程当中,咱们须要实时知道的东西。函数
这些信息都准备以block回调的形式展示,具体以下:工具
/** * 无参数block */ typedef void(^DownloaderCreateBlock)(); /** * 下载回调信息,下载进度Block * * @param AlreadyReceiveSize 已经接收大小 * @param NotReceiveSize 未接收大小 */ typedef void(^DownloaderProgressBlock)(NSInteger alreadyReceiveSize,NSInteger expectedContentLength); /** * 下载回调信息,完成下载Block * * @param data data * @param image 图片 * @param error 错误信息 * @param finished 是否完成 */ typedef void(^DownloaderCompletedBlock)(NSData *data,UIImage *image,NSError *error,BOOL finished);
在整个下载中,咱们还须要有一些配置选项,例如是否容许后台下载,选择队列下载方式,仍是栈的下载方式.因此设置了如下的选项。测试
typedef NS_OPTIONS(NSInteger,DownloaderOptions) { //默认下载操做 DownloaderDefault = 1, //容许后台操做 DownloaderContinueInBackground = 2 }; typedef NS_ENUM(NSInteger,DownloaderOrder){ //默认下载顺序,先进先出 DownloaderFIFO, //先进后出 DownloaderLIFO };
基本的信息构建完成,我考虑的就是须要将这些状态的回调信息存在一个NSMutableDictionary中,key值就是咱们的下载地址,value就是NSMutableArray,里面包含所DownloaderProgressBlock,DownloaderCompletedBlock进度信息。
定义了一下属性:
/** * 将全部的下载回调信息存储在这里,Key是URL,Value是多组回调信息 */ @property(strong,nonatomic) NSMutableDictionary *downloaderCallBack; 在一个下载开始以前,须要加载,或者是删除一些状态信息,构建了如下的函数。 /** * 添加回调信息 * * @param progressBlock DownloaderProgressBlock * @param completedBlock DownloaderCompletedBlock * @param url url * @param DownloaderCreateBlock DownloaderCreateBlock */ -(void)addWithDownloaderProgressBlock:(DownloaderProgressBlock)progressBlock DownloaderCompletedBlock:(DownloaderCompletedBlock)completedBlock URL:(NSURL *)url DownloaderCreateBlock:(DownloaderCreateBlock)downloaderCreateBlock{ /** * 判断url是否为空 */ if ([url isEqual:nil]) { completedBlock(nil,nil,nil,NO); } /** * 设置屏障,保证在同一时间,只有一个线程能够操做downloaderCallBack属性,保证在并行多个处理的时候,对downloaderCallBack属性的读写操做保持一致 */ dispatch_barrier_sync(self.concurrentQueue, ^{ BOOL firstDownload = NO; /** * 添加回调信息,处理同一个url信息。 */ if(!self.downloaderCallBack[url]){ self.downloaderCallBack[url] = [NSMutableArray new]; firstDownload = YES; } NSMutableArray *callBacksArray = self.downloaderCallBack[url]; NSMutableDictionary *callBacks = [[NSMutableDictionary alloc] init]; if (progressBlock) { callBacks[@"progress"] = [progressBlock copy]; } if (completedBlock) { callBacks[@"completed"] = [completedBlock copy]; } [callBacksArray addObject:callBacks]; self.downloaderCallBack[url] = callBacksArray; if (firstDownload) { downloaderCreateBlock(); } }); }
首先就是判断当前的url是否为空,若是为空,直接回调空处理。不为空的话,为了防止同一URL的value被重复建立,咱们须要在这里判断下原来是否存在,是否为第一次下载,是第一下下载的话,这里咱们会触发adownloaderCreateBlock()的回调来进行operation的配置,固然若是不是第一次,我就仅仅须要把这个新的DownloaderProgressBlock,DownloaderCompletedBlock放进callBacksArray中便可。
这里为了保证downloaderCallBack的线程安全性,咱们加了一个屏障,来保证每次只有一个线程操做downloaderCallBack属性。
这么作的一个好处就是,我每个下载,我判断一下是否是同一URL,是的话我就作伪下载,就是感受上下载,可是不下载,而后已经正在下载进度会同时反馈给当前其余相同的下载请求。
整个下载管理器,咱们须要将下载在一个模块中被管理。就像下面这样
/** * 下载管理器对于下载请求的管理 * * @param progressBlock DownloaderProgressBlock * @param completedBlock DownloaderCompletedBlock * @param url url */ -(void)downloaderImageWithDownloaderWithURL:(NSURL *)url DownloaderProgressBlock:(DownloaderProgressBlock)progressBlock DownloaderCompletedBlock:(DownloaderCompletedBlock)completedBlock{ __weak __typeof(self)myself = self; __block LYDownloaderOperation *operation; [self addWithDownloaderProgressBlock:progressBlock DownloaderCompletedBlock:completedBlock URL:url DownloaderCreateBlock:^{ NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy: NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:20]; operation = [[LYDownloaderOperation alloc] initWithRequest:request DownloaderOptions:1 DownloaderProgressBlock:^(NSInteger alreadyReceiveSize,NSInteger expectedContentLength){ __block NSArray *urlCallBacks; dispatch_sync(self.concurrentQueue, ^{ urlCallBacks = [myself.downloaderCallBack[url] copy]; }); for (NSDictionary *callbacks in urlCallBacks) { dispatch_async(dispatch_get_main_queue(), ^{ DownloaderProgressBlock progress = callbacks[@"progress"]; if (progress) { progress(alreadyReceiveSize,expectedContentLength); } }); } } DownloaderCompletedBlock:^(NSData *data,UIImage *image,NSError *error,BOOL finished){ completedBlock(data,image,error,finished); } cancelled:^{ }]; [myself.downloadQueue addOperation:operation]; }]; }
这部分主要就是在配置咱们的operation,将配置完成后的operation添加到下载队列。
/** * 下载队列 */ @property(strong,nonatomic) NSOperationQueue *downloadQueue; [myself.downloadQueue addOperation:operation]; 在这里: DownloaderProgressBlock:^(NSInteger alreadyReceiveSize,NSInteger expectedContentLength){ __block NSArray *urlCallBacks; dispatch_sync(self.concurrentQueue, ^{ urlCallBacks = [myself.downloaderCallBack[url] copy]; }); for (NSDictionary *callbacks in urlCallBacks) { dispatch_async(dispatch_get_main_queue(), ^{ DownloaderProgressBlock progress = callbacks[@"progress"]; if (progress) { progress(alreadyReceiveSize,expectedContentLength); } }); } }
就是进度的回调通知,在这里能够知道,若是咱们使用了GCD,保证通知对象加载完整后在进行通知
dispatch_sync(self.concurrentQueue, ^{ urlCallBacks = [myself.downloaderCallBack[url] copy]; });
这里使用同步保证了咱们进度被通知对象的完整性。
接下来的话就是异步回调通知了,下面的其余地方的基本结构也都作了相似的处理。主要就是保证每一条下载线程,每一条通知都安全的进行着。在完成下载的时候移除对应url的状态,这里也是为了保证downloaderCallBack的线程安全性,咱们加了一个屏障,来保证每次只有一个线程操做downloaderCallBack属性。
这里开始就是作下载处理了。须要作的就是重写start方法,在这里建立而且配置NSURLSession对象。
-(void)start{ NSLog(@"start"); /** * 建立NSURLSessionConfiguration类的对象, 这个对象被用于建立NSURLSession类的对象. */ NSURLSessionConfiguration *configura = [NSURLSessionConfiguration defaultSessionConfiguration]; /** * 2. 建立NSURLSession的对象. * 参数一 : NSURLSessionConfiguration类的对象.(第1步建立的对象.) * 参数二 : session的代理人. 若是为nil, 系统将会提供一个代理人. * 参数三 : 一个队列, 代理方法在这个队列中执行. 若是为nil, 系统会自动建立一系列的队列. * 注: 只能经过这个方法给session设置代理人, 由于在NSURLSession中delegate属性是只读的. */ NSURLSession *session = [NSURLSession sessionWithConfiguration:configura delegate:self delegateQueue:nil]; /** * 建立request */ NSMutableURLRequest *request = self.request; /** * 建立数据类型任务 */ NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request]; /** * 开始任务 */ [dataTask resume]; /** * 在session中的全部任务都完成以后, 使session失效. */ [session finishTasksAndInvalidate]; }
由于咱们实现了NSURLSessionDataDelegate协议,因此能够自定义一些操做。
//最早调用,在这里作一些数据的初始化。 -(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler{ NSLog(@"开始"); self.imageData = [[NSMutableData alloc] init]; self.expectedContentLength = response.expectedContentLength; if (self.progressBlock) { self.progressBlock(0,self.expectedContentLength); } completionHandler(NSURLSessionResponseAllow); } //下载响应 - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data{ [self.imageData appendData:data]; if (self.progressBlock) { self.progressBlock(self.imageData.length,self.expectedContentLength); } } //下载完成后调用 -(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error{ self.completedBlock(self.imageData,nil,error,YES); [self cancel]; }
重写了以上的一些实现。
最后强调下这里:
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy: NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:30];
为了不潜在的重复缓存,NSURLCache与本身的缓存方案,则禁用图片请求的缓存操做。
基本上咱们就能够进行测试了:
写了一个简单UI,用来展现下载部分
缓存这个地方须要考虑的东西仍是不少的,那么将会针对一下问题进行描述,设计。
直接上代码:
/** * 进行缓存 * * @param memoryCache 内存 * @param image 图片 * @param imageData 图片data * @param urlKey key值就用来惟一标记数据 * @param isSaveTOdisk 是否进行沙箱缓存 */ -(void)saveImageWithMemoryCache:(NSCache *)memoryCache image:(UIImage *)image imageData:(NSData *)imageData urlKey:(NSString *)urlKey isSaveToDisk:(BOOL)isSaveToDisk{ //内存缓存 if ([memoryCache isEqual:nil]) { [self.memoryCache setObject:image forKey:urlKey]; }else{ [memoryCache setObject:image forKey:urlKey]; } //磁盘缓存 if (isSaveToDisk) { dispatch_sync(self.ioSerialQueue, ^{ if (![_fileMange fileExistsAtPath:_diskCachePath]) { [_fileMange createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:nil]; } NSString *pathForKey = [self defaultCachePathForKey:urlKey]; NSLog(@"%@",pathForKey); [_fileMange createFileAtPath:pathForKey contents:imageData attributes:nil]; }); } }
注释中基本上就描述了各部分的职责,首先就是内存缓存,这里的内存缓存我用NSCache进行处理,这里用NSCache就是其实就是一个集合类型,这个集合类型维护这一个key-value结构。当某一些对象销毁的代价,或者从新生成的代价高于咱们内存保留。那么咱们就把它内存缓存下来就是很值的。所以重用这些对象是很值得的,毕竟咱们不须要二次计算了,而且当咱们的内存警报的时候,他本身会丢弃掉一些没用的东西的。就像代码中标记的,就是作了内存缓存工做。
磁盘缓存使用NSFileManager实现,存放的位置就是沙箱的Cache文件夹内,这样就能够了。而且咱们能够看到,这里咱们是能够根据isSaveToDisk来判断是否须要进行磁盘的缓存,由于有一些东西是不须要缓存在磁盘中的,另外,异步操做也是很关键的一个地方,一样咱们在这里使用dispatch_sync来作一些处理,实现咱们的异步操做。而且这里的文件名实使用是将URL变换为MD5值。保证了惟一性。
缓存的操做基本上就完成了,既然能存,就须要对应查询。
//查询图片 -(void)selectImageWithKey:(NSString *)urlKey completedBlock:(CompletedBlock)completed{ UIImage *image = [self.memoryCache objectForKey:urlKey]; if ([image isEqual:nil]) { NSLog(@"ok"); completed(image,nil,ImageCacheTypeMemory); }else{ NSString *pathForKey = [self defaultCachePathForKey:urlKey]; NSLog(@"%@",pathForKey); NSData *imageData = [NSData dataWithContentsOfFile:pathForKey]; UIImage *diskImage = [UIImage imageWithData:imageData]; completed(diskImage,nil,ImageCacheTypeDisk); } }
这里的查询基本上就是两种方式,第一种若是内存中存在,那么就在内存中读取就能够了。固然也存在着内存中不存在的可能性,这样就须要从磁盘中开始读取信息数据。根据MD5值进行索引,而后block回调给上层数据信息进行处理。
最后就是删除操做,由于若是咱们设置了磁盘的上限,当咱们设定的磁盘空间达到上限的时候该怎么作?当咱们想清空全部缓存的时候,咱们该怎么作呢?下面的这两段代码就是为了作清理磁盘空间的事情的。
/** * 清空所有 * * @param completion completion */ - (void)clearDiskOnCompletion:(NoParamsBlock)completion { dispatch_async(self.ioSerialQueue, ^{ [_fileMange removeItemAtPath:self.diskCachePath error:nil]; [_fileMange createDirectoryAtPath:self.diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL]; if (completion) { dispatch_async(dispatch_get_main_queue(), ^{ completion(); }); } }); }
这段代码的做用就是为了作清空磁盘的做用,一样是使用NSFileManager来实现。
/** * 按条件进行清空(主要是时间),这里盗用了SDWebImage的设计 * * @param noParamsBlock completion */ -(void)clearDiskWithNoParamsBlock:(NoParamsBlock)noParamsBlock{ dispatch_async(self.ioSerialQueue, ^{ NSURL *diskCache = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES]; NSArray *resourcKeys = @[NSURLIsDirectoryKey,NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey]; // 1. 该枚举器预先获取缓存文件的有用的属性 NSDirectoryEnumerator *fileEnumerator = [_fileMange enumeratorAtURL:diskCache includingPropertiesForKeys:resourcKeys options:NSDirectoryEnumerationSkipsHiddenFiles errorHandler:NULL]; NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-60 * 60 * 24 * 7]; NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary]; NSInteger currentCacheSize = 0; NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init]; for (NSURL *fileURL in fileEnumerator) { NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourcKeys error:NULL]; // 3. 跳过文件夹 if ([resourceValues[NSURLIsDirectoryKey] boolValue]) { continue; } NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey]; if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) { [urlsToDelete addObject:fileURL]; continue; } // 5. 存储文件的引用并计算全部文件的总大小,以备后用 NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey]; currentCacheSize += [totalAllocatedSize unsignedIntegerValue]; [cacheFiles setObject:resourceValues forKey:fileURL]; } for (NSURL *fileURL in urlsToDelete) { [self.fileMange removeItemAtURL:fileURL error:NULL]; } if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) { const NSUInteger desiredCacheSize = self.maxCacheSize / 2; // Sort the remaining cache files by their last modification time (oldest first). NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent usingComparator:^NSComparisonResult(id obj1, id obj2) { return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]]; }]; // Delete files until we fall below our desired cache size. for (NSURL *fileURL in sortedFiles) { if ([_fileMange removeItemAtURL:fileURL error:nil]) { NSDictionary *resourceValues = cacheFiles[fileURL]; NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey]; currentCacheSize -= [totalAllocatedSize unsignedIntegerValue]; if (currentCacheSize < desiredCacheSize) { break; } } } } if (noParamsBlock) { dispatch_async(dispatch_get_main_queue(), ^{ noParamsBlock(); }); } }); }
这段代码就是实现了部分的清理工做,清理的工做就是根据咱们设定的一些参数来实现的,这里包含着咱们设定的缓存有效期,缓存的最大空间是多少。过了咱们设定的有效期,这个时候咱们就须要去清理掉这部份内容。另外若是若是全部缓存文件的总大小超过这一大小,则会按照文件最后修改时间的逆序,以每次一半的递归来移除那些过早的文件,直到缓存的实际大小小于咱们设置的最大使用空间
到了这里咱们的缓存的方案就基本上完成了,咱们作一个小小的测试,从本地读取一个文件,而后缓存到cache文件加剧,而后进行加载,看一下效果。来看一下测试代码:
- (IBAction)read:(id)sender { LDImageCache *l = [LDImageCache shareLDImageCache]; UIImage *myImage = [UIImage imageNamed:@"author.jpg"]; NSData *data = UIImagePNGRepresentation(myImage); [l saveImageWithMemoryCache:nil image:myImage imageData:data urlKey:@"lastdays.cn" isSaveToDisk:YES]; [l selectImageWithKey:@"lastdays.cn" completedBlock:^(UIImage *image,NSError *error,ImageCacheType type){ NSLog(@"%ld",(long)type); [self.image setImage:image]; }]; // [l clearDiskOnCompletion:^{ // NSLog(@"完成清空"); // }]; }
这里的读取数据,首先咱们从本地读取一个图片,而后调用saveImageWithMemoryCache:将图片数据缓存在内存和磁盘中。而后根据key值调用selectImageWithKey进行查询,这里咱们输出的ImageCacheType数据,查看一下都是从哪里进行的读取。咱们会优先从内存中进行读取数据。