圖片緩存的方案

下載 

下載的話,我查閱了不少人寫的,像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屬性。

子類化NSOperation 

這裏開始就是作下載處理了。須要作的就是重寫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數據,查看一下都是從哪裏進行的讀取。咱們會優先從內存中進行讀取數據。

相關文章
相關標籤/搜索