下載的話,我查閱了不少人寫的,像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數據,查看一下都是從哪裏進行的讀取。咱們會優先從內存中進行讀取數據。