iOS:緩存與Operation優先級問題

這篇博客來源於今年的一個面試題,當咱們使用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;
        });
    }];

 

  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裏面找下,若是沒有,那麼查看下磁盤緩存中是否有這樣的圖片,若是沒有,看下這張圖片是否正在被下載,若是仍是沒有,就開始下載這張圖片,代碼: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;  //加入字典,表示正在執行這次操做
                }
            }
        }
    

     

  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

相關文章
相關標籤/搜索