iOS-SDWebImage源碼解讀

大神原文地址:http://blog.csdn.net/deft_mkjing/article/details/52900586

SD內(nèi)部已經(jīng)幫我們把請求回來的數(shù)據(jù)或者緩存到本地的圖片資源都進(jìn)行了異步解壓縮院尔,因此不需要我們來做,簡單了解下

一. 圖片壓縮流程

  1. 假設(shè)我們使用 +imageWithContentsOfFile: 方法從磁盤中加載一張圖片,這個時候的圖片并沒有解壓縮;
  2. 然后將生成的 UIImage 賦值給 UIImageView
  3. 接著一個隱式的 CATransaction 捕獲到了 UIImageView 圖層樹的變化蜡塌;
  4. 在主線程的下一個 run loop 到來時,Core Animation 提交了這個隱式的 transaction ,這個過程可能會對圖片進(jìn)行 copy 操作蠕搜,而受圖片是否字節(jié)對齊等因素的影響,這個 copy 操作可能會涉及以下部分或全部步驟:
    1. 分配內(nèi)存緩沖區(qū)用于管理文件 IO 和解壓縮操作收壕;
    2. 將文件數(shù)據(jù)從磁盤讀到內(nèi)存中妓灌;
    3. 將壓縮的圖片數(shù)據(jù)解碼成未壓縮的位圖形式轨蛤,這是一個非常耗時的 CPU 操作;
    4. 最后 Core Animation 使用未壓縮的位圖數(shù)據(jù)渲染 UIImageView 的圖層虫埂。

在上面的步驟中祥山,我們提到了圖片的解壓縮是一個非常耗時的 CPU 操作,并且它默認(rèn)是在主線程中執(zhí)行的掉伏。那么當(dāng)需要加載的圖片比較多時缝呕,就會對我們應(yīng)用的響應(yīng)性造成嚴(yán)重的影響,尤其是在快速滑動的列表上斧散,這個問題會表現(xiàn)得更加突出岳颇。

可以理解成:
1.首先PNG和JPEG都是圖片的壓縮格式,PNG是無損壓縮颅湘,支持alpha话侧,JPEG有損,可選1-100壓縮比例.
2.例如你有一張PNG的圖片 20B闯参,那么你對應(yīng)的圖片二進(jìn)制數(shù)據(jù)也是20B的瞻鹏,解壓縮后的圖片就可能是幾百幾千B了.
3.具體計(jì)算公式就是像素寬像素高每個像素對應(yīng)的字節(jié).
4.那么當(dāng)你進(jìn)行圖片渲染的時候,必須得到解壓縮后的原始像素?cái)?shù)據(jù)鹿寨,才能進(jìn)行圖形渲染新博,這就是解壓縮的原因.

SD在SDWebImageDecoder這個文件中進(jìn)行了強(qiáng)制解壓縮,我們賦值給imageView的時候已經(jīng)是解壓縮的文件了脚草,因此不會卡主主線程赫悄,不然默認(rèn)是在主線程進(jìn)行解壓縮,圖片一多馏慨,卡爆了

image

這個圖分四個部分埂淮,聽完我的分析你就能基本上理解了

二. SD用過的類簡單介紹

NSData+ImageContentType 通過Image data判斷當(dāng)前圖片的格式

SDImageCache 緩存 定義了 Disk 和 memory二級緩存(NSCache)負(fù)責(zé)管理cache 單例

SDWebImageCompat 保證不同平臺/版本/屏幕等兼容性的宏定義和內(nèi)聯(lián) 圖片縮放

SDWebImageDecoder 圖片解壓縮,內(nèi)部只有一個接口

SDWebImageDownloader 異步圖片下載管理写隶,管理下載隊(duì)列倔撞,管理operation 管理網(wǎng)絡(luò)請求 處理結(jié)果和異常 單例

存放網(wǎng)絡(luò)請求回調(diào)的block 自己理解的數(shù)據(jù)結(jié)構(gòu)大概是

// 結(jié)構(gòu){"url":[{"progress":"progressBlock"},{"complete":"completeBlock"}]}

SDWebImageDownloaderOperation 實(shí)現(xiàn)了異步下載圖片的NSOperation,網(wǎng)絡(luò)請求給予NSURLSession 代理下載

自定義的Operation任務(wù)對象慕趴,需要手動實(shí)現(xiàn)start cancel等方法

SDWebImageManager 核心管理類 主要對緩存管理 + 下載管理進(jìn)行了封裝 主要接口downloadImageWithURL單利

SDWebImageOperation operation協(xié)議 只定義了cancel operation這一接口 上面的downloaderOperation的代理

SDWebImagePrefetcher 低優(yōu)先級情況下預(yù)先下載圖片痪蝇,對SDWebImageViewManager進(jìn)行簡單封裝 很少用

MKAnnotationView+WebCache – 為MKAnnotationView異步加載圖片

UIButton+WebCache 為UIButton異步加載圖片

UIImage+GIF 將Image data轉(zhuǎn)換成指定格式圖片

UIImage+MultiFormat 將image data轉(zhuǎn)換成指定格式圖片

UIImageView+HighlightedWebCache 為UIImageView異步加載圖片

UIImageView+WebCache 為UIImageView異步加載圖片

UIView+WebCacheOperation 保存當(dāng)前MKAnnotationView / UIButton / UIImageView異步下載圖片的operations

看下核心類的基本操作圖
image

三. SDWebImage原理分析

1.第一步(外部控件)

__weak typeof(self)weakSelf = self;
[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://cdn.duitang.com/uploads/item/201111/08/20111108113800_wYcvP.thumb.600_0.jpg"] placeholderImage:nil completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
    if (image && cacheType == SDImageCacheTypeNone)
    {
        weakSelf.imageView.alpha = 0;
        [UIView animateWithDuration:1.0f animations:^{
            weakSelf.imageView.alpha = 1.f;
        }];
    }
    else
    {
        weakSelf.imageView.alpha = 1.0f;
    }
}];

這個很簡單,大家也經(jīng)常用冕房,用的時候打一下sd機(jī)會出來一串方法躏啰,這一串方法最終在內(nèi)部都會轉(zhuǎn)換成.

[self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:completedBlock];

2.第二步(UIImageView + WebCache)

// 以上所有的調(diào)用方法,最終都是進(jìn)入這個方法進(jìn)行圖片的加載
// url  加載的圖片
// placeholder 占位圖
// options 下載圖片的各種花式設(shè)置  一般使用的是SDWebImageRetryFailed | SDWebImageLowPriority
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
    
    // 取消當(dāng)前的下載操作 如果不取消耙册,那么當(dāng)tableView滑動的時候给僵,當(dāng)前cell的imageView會一直去下載圖片,然后優(yōu)先顯示下載完成的圖片觅玻,直接錯亂
    [self sd_cancelCurrentImageLoad];
    
    {......省略一段代碼}
    // 判斷是否存在
    if (url) {
        {......省略一段代碼}
        __weak __typeof(self)wself = self;
        // 關(guān)鍵類 SDWebImageManager 來處理圖片下載
        // 下載有三層 1當(dāng)前manager調(diào)用下載  2從緩存中獲取想际,hit失敗用SDWebImageDownloader對象調(diào)用下載 3.SDWebImageDownloaderOperation最終用繼承
        // NSOperation的對象NSURLSession的方法去下載圖片培漏,代理里面進(jìn)行操作
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            [wself removeActivityIndicator];
            if (!wself) return;
            dispatch_main_sync_safe(^{
                if (!wself) return;
                // 設(shè)置了SDWebImageAvoidAutoSetImage 默認(rèn)不會將UIImage添加進(jìn)UIImageView對象里面,而放置在conpleteBlock里面交由調(diào)用方自己處理胡本,做個濾鏡或者淡入淡出什么的
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
                {
                    completedBlock(image, error, cacheType, url);
                    return;
                }
                else if (image) {
                    wself.image = image;
                    [wself setNeedsLayout];
                } else {
                    if ((options & SDWebImageDelayPlaceholder)) {
                        wself.image = placeholder;
                        [wself setNeedsLayout];
                    }
                }
                // 這里是最終回調(diào)出去的block
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        // 保存本次operation,如果發(fā)生多次圖片請求可以用來取消
        // 先取消當(dāng)前UIImageView正在下載的任務(wù)牌柄,然后在保存operations
        // 也就是說當(dāng)動態(tài)綁定的字典里面的key value對應(yīng)一個圖片下載 單個圖片value數(shù)組就是0,不然就是多個侧甫,下載完就會根據(jù)key移除
        [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
    } else {
        dispatch_main_async_safe(^{
            [self removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

分析下:

這里首先會調(diào)用cancel的方法珊佣,為什么會這樣呢?披粟?解釋下會發(fā)生的bug

正常情況下屏幕展示的cell是這樣的咒锻,當(dāng)網(wǎng)絡(luò)不穩(wěn)定的情況下,第一個cell還在請求數(shù)據(jù)的時候守屉,用戶直接下滑了

就會出現(xiàn)下面這樣的狀態(tài)惑艇,這個是tableView的復(fù)用簡單圖解,當(dāng)列表后面的cell準(zhǔn)備加載的時候拇泛,會從復(fù)用池中找滨巴,有的話直接拿出來用,那么復(fù)用池子里面的cell還是第一個cell指向的imageView指針俺叭,如果沒有停止之前的網(wǎng)絡(luò)請求恭取,那么直接拿出來再根據(jù)后面的數(shù)據(jù)綁定進(jìn)行請求就會發(fā)生數(shù)據(jù)錯位,這是非诚ㄊ兀可怕的蜈垮。原因就是如果沒有取消之前的請求,imageView的原理就是優(yōu)先展示最新下載完之后的圖片裕照,就會立馬顯示出來攒发,所以一定要先取消之前的

再來個例子:

一個imageView請求了兩張圖片,1.png 和 2.png牍氛,但我們只希望顯示 2.png晨继,所以需要取消 1.png的請求烟阐。原因有兩點(diǎn):

1.在異步請求中(先后順序不定)搬俊,有可能 1.png 會在 2.png 后面獲取到,會覆蓋掉2.png
2.減少網(wǎng)絡(luò)請求蜒茄,網(wǎng)絡(luò)請求是一個很耗時的操作

然后這一步的重點(diǎn)就是會啟用SDWebImageManager管理單例唉擂,調(diào)用他的方法進(jìn)行網(wǎng)絡(luò)請求

1.  - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url  
2.  options:(SDWebImageOptions)options  
3.  progress:(SDWebImageDownloaderProgressBlock)progressBlock  
4.  completed:(SDWebImageCompletionWithFinishedBlock)completedBlock   

3. 第三步(SDWebImageManager)

這個管理類有兩個得力的手下

一個是SDImageCache 專門管理緩存

A:NSCache負(fù)責(zé)內(nèi)存緩存,用法和NSDictionary基本一樣
B:磁盤緩存用NSFileManager寫文件的方式完成(圖片轉(zhuǎn)換成NSData)

注:

1. NSCache具有自動刪除的功能檀葛,以減少系統(tǒng)占用的內(nèi)存玩祟,還能設(shè)置內(nèi)存臨界值
2. NSCache是線程安全的,不需要加線程鎖屿聋;
3. 鍵對象不會像 NSMutableDictionary 中那樣被復(fù)制空扎。(鍵不需要實(shí)現(xiàn) NSCopying 協(xié)議)藏鹊。

// 從imageCache中尋找圖片
//每次向SDWebImageCache索取圖片的時候,會先根據(jù)圖片URL對應(yīng)的key值先檢查內(nèi)存中是否有對應(yīng)的圖片转锈,如果有則直接返回盘寡;如果沒有則在ioQueue中去硬盤中查找,其中文件名是是根據(jù)URL生成的MD5值撮慨,找到之后先將圖片緩存在內(nèi)存中竿痰,然后在把圖片返回:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
    if (!doneBlock) {
        return nil;
    }
    
    if (!key) {
        doneBlock(nil, SDImageCacheTypeNone);
        return nil;
    }
    
    // First check the in-memory cache...
    // 1.先去內(nèi)存層面查找
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        doneBlock(image, SDImageCacheTypeMemory);
        return nil;
    }
    
    // 如果在內(nèi)存沒找到
    // 2. 如果內(nèi)存中沒有,則在磁盤中查找砌溺。如果找到影涉,則將其放到內(nèi)存緩存,并調(diào)用doneBlock回調(diào)
    NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            return;
        }
        // 創(chuàng)建自動釋放池规伐,內(nèi)存即時釋放
        //        如果你的應(yīng)用程序或者線程是要長期運(yùn)行的并且有可能產(chǎn)生大量autoreleased對象, 你應(yīng)該使用autorelease pool blocks
        @autoreleasepool {
            // 從硬盤拿蟹倾,拿到了根據(jù)字段存入內(nèi)存
            UIImage *diskImage = [self diskImageForKey:key];
            if (diskImage && self.shouldCacheImagesInMemory) {
                // 像素
                NSUInteger cost = SDCacheCostForImage(diskImage);
                // 緩存到NSCache中
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }
            
            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, SDImageCacheTypeDisk);
            });
        }
    });
    
    return operation;
}

小知識點(diǎn):

這里開了異步串行隊(duì)列去Disk中查找,保證不阻塞主線程猖闪,而且開了autoreleasepool以降低內(nèi)存暴漲問題喊式,能得到及時釋放,如果能取到萧朝,首先緩存到內(nèi)存中然后再回調(diào)

如果內(nèi)存和磁盤中都取不到圖片岔留,就會讓Manager的另一個手下SDWebImageDownloader去下載圖片

A:這貨也是一個單例,專門負(fù)責(zé)圖片的下載圖片的下載都是放在NSOperationQueue中完成的

// 下載圖片的最終方法實(shí)現(xiàn)
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
    {......}
    // 根據(jù)URL生成對應(yīng)的key  沒有特殊處理為[self absoluteString]
    NSString *key = [self cacheKeyForURL:url];
    
    // 前面的操作检柬。主要作了如下處理:
    // 1. 判斷url的合法性
    // 2. 創(chuàng)建SDWebImageCombinedOperation對象
    // 3. 查看url是否是之前下載失敗過的
    // 去imageCache中尋找圖片
    operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
        if (operation.isCancelled) {
            @synchronized (self.runningOperations) {
                [self.runningOperations removeObject:operation];
            }
            
            return;
        }
        // 如果圖片沒有找到 或者是SDWebImageRefreshCached 就從網(wǎng)絡(luò)上下載圖片
        
        if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
            if (image && options & SDWebImageRefreshCached) {
                dispatch_main_sync_safe(^{
                    // 如果圖片存在cache中献联,但是options還是SDWebImageRefreshCached 通知cache去重新刷新緩存圖片
                    completedBlock(image, nil, cacheType, YES, url);
                });
            }
            
            // 設(shè)置下載選項(xiàng)屬性
            {......}
            // 開啟下載
            // 這里的兩個回調(diào)都是從DownloaderOperation里面出來的,progressBlock是不要取到的何址,直接在最外層調(diào)用的地方處理里逆,完成的話需要進(jìn)行cache,因此要在這里處理回調(diào)用爪,處理完再回調(diào)出去
            id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
                // 上面的是weak的這里設(shè)置成strong 避免被釋放掉了
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                
                if (!strongOperation || strongOperation.isCancelled) {
                    // Do nothing if the operation was cancelled
                    // See #699 for more details
                    // if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data
                }
                else if (error) {
                    // 如果出錯原押,則調(diào)用完成回調(diào),并將url放入下載挫敗url數(shù)組中
                    dispatch_main_sync_safe(^{
                        if (strongOperation && !strongOperation.isCancelled) {
                            completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
                        }
                    });
                    
                    if (   error.code != NSURLErrorNotConnectedToInternet
                        && error.code != NSURLErrorCancelled
                        && error.code != NSURLErrorTimedOut
                        && error.code != NSURLErrorInternationalRoamingOff
                        && error.code != NSURLErrorDataNotAllowed
                        && error.code != NSURLErrorCannotFindHost
                        && error.code != NSURLErrorCannotConnectToHost) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs addObject:url];
                        }
                    }
                }
                else {
                    // 當(dāng)重新下載的時候能獲取到了偎血,那么久把他從之前的failURL里面移除
                    if ((options & SDWebImageRetryFailed)) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs removeObject:url];
                        }
                    }
                    // 是否硬盤緩存
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
                    
                    if (options & SDWebImageRefreshCached && image && !downloadedImage) {
                        // Image refresh hit the NSURLCache cache, do not call the completion block
                    }
                    else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
                        
                        // 在全局隊(duì)列中并行處理圖片的緩存
                        // 首先對圖片做個轉(zhuǎn)換操作诸衔,該操作是代理對象實(shí)現(xiàn)的
                        // 然后對圖片做緩存處理
                        
                        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                            UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
                            
                            if (transformedImage && finished) {
                                BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                                // 二級緩存起來
                                [self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
                            }
                            
                            dispatch_main_sync_safe(^{
                                if (strongOperation && !strongOperation.isCancelled) {
                                    completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
                                }
                            });
                        });
                    }
                    else {
                        // 下載完成后之后 先cache起來 內(nèi)存緩存和磁盤緩存都要考慮
                        if (downloadedImage && finished) {
                            [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
                        }
                        
                        dispatch_main_sync_safe(^{
                            if (strongOperation && !strongOperation.isCancelled) {
                                completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
                            }
                        });
                    }
                }
                
                // 完成之后也要移除掉
                if (finished) {
                    @synchronized (self.runningOperations) {
                        if (strongOperation) {
                            [self.runningOperations removeObject:strongOperation];
                        }
                    }
                }
            }];
            operation.cancelBlock = ^{
                [subOperation cancel];
                
                @synchronized (self.runningOperations) {
                    __strong __typeof(weakOperation) strongOperation = weakOperation;
                    if (strongOperation) {
                        [self.runningOperations removeObject:strongOperation];
                    }
                }
            };
        }
        else if (image) {
            // 如果圖片存在 直接返回
            dispatch_main_sync_safe(^{
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                if (strongOperation && !strongOperation.isCancelled) {
                    completedBlock(image, nil, cacheType, YES, url);
                }
            });
            @synchronized (self.runningOperations) {
                [self.runningOperations removeObject:operation];
            }
        }
        else {
            // 如果沒有cacahe 而且沒實(shí)現(xiàn)代理下載,直接返回nil
            // Image not in cache and download disallowed by delegate
            dispatch_main_sync_safe(^{
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                if (strongOperation && !weakOperation.isCancelled) {
                    completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
                }
            });
            // 移除
            @synchronized (self.runningOperations) {
                [self.runningOperations removeObject:operation];
            }
        }
    }];
    
    return operation;
}

4. 第四步(SDWebImageDownloader)

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
    __block SDWebImageDownloaderOperation *operation;
    __weak __typeof(self)wself = self;
    
    // 290行颇玷,同一個方法里面笨农,進(jìn)行urlCallBacks的組裝
    // 該方法有點(diǎn)明白了,就是讓同一個url值生成一個createCallBack -->也就是只出來一個operation任務(wù)
    // wself.downloadQueue同一url只會加入一次帖渠,但是多次重復(fù)請求谒亦,urlcallbacks的url數(shù)組里面會有多個回調(diào)block,這個不影響,只要正真下載一次就好了份招,回調(diào)可以遍歷切揭,下面下載完都一直在遍歷,沒錯锁摔,這就對了
    [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
        NSTimeInterval timeoutInterval = wself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }
        // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
        // 防止NSURLCache 和 SDImageCache重復(fù)緩存 如果沒有明確告知需要緩存伴箩,則禁用圖片請求的緩存操作
        // 1. 創(chuàng)建請求對象,并根據(jù)options參數(shù)設(shè)置其屬性
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        // 設(shè)置http頭部
        if (wself.headersFilter) {
            request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
        }
        else {
            request.allHTTPHeaderFields = wself.HTTPHeaders;
        }
        //SDWebImageDownloaderOperation派生自NSOperation鄙漏,負(fù)責(zé)圖片下載工作
        // 2. 創(chuàng)建SDWebImageDownloaderOperation操作對象嗤谚,并進(jìn)行配置
        // SDWebImageDownloaderOperation class
        operation = [[wself.operationClass alloc] initWithRequest:request
                                                        inSession:self.session
                                                          options:options
                                                         progress:^(NSInteger receivedSize, NSInteger expectedSize) {
            SDWebImageDownloader *sself = wself;
            if (!sself) return;
            __block NSArray *callbacksForURL;
            // 3. 從管理器的callbacksForURL中找出該URL所有的進(jìn)度處理回調(diào)并調(diào)用
            // 這個barrierQueue是并發(fā)的,如果是get main queue的話就死鎖了
            // 我個人感覺去掉直接寫也問題不大怔蚌,不知道為什么這么寫巩步??桦踊?反正是順序執(zhí)行
            dispatch_sync(sself.barrierQueue, ^{
                // URLCallbacks是mutale字典對象
                callbacksForURL = [sself.URLCallbacks[url] copy];
            });
            // 進(jìn)度正常椅野,肯定有多個block
            for (NSDictionary *callbacks in callbacksForURL) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
                    // 正在下載的時候回傳已經(jīng)收到的size和totalsize出去
                    if (callback) callback(receivedSize, expectedSize);
                });
            }
        }
                     // 下載完之后會進(jìn)到這個回調(diào),然后我們用之前存起來的回調(diào)再回調(diào)出去
                                                        completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
            SDWebImageDownloader *sself = wself;
            if (!sself) return;
            __block NSArray *callbacksForURL;
            // 4. 從管理器的callbacksForURL中找出該URL所有的完成處理回調(diào)并調(diào)用籍胯,
            // 如果finished為YES竟闪,則將該url對應(yīng)的回調(diào)信息從URLCallbacks中刪除
            // 個人理解阻塞當(dāng)前線程,而且barrierQueue也阻塞 那么當(dāng)同一個URL完成的時候直接沒了對象杖狼,重復(fù)下載也沒用了
            dispatch_barrier_sync(sself.barrierQueue, ^{
                callbacksForURL = [sself.URLCallbacks[url] copy];
                if (finished) {
                    [sself.URLCallbacks removeObjectForKey:url];
                }
            });
            // 這里一般只有一個炼蛤,但是多次重復(fù)請求
            for (NSDictionary *callbacks in callbacksForURL) {
                SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
                if (callback) callback(image, data, error, finished);
            }
        }
                                                        cancelled:^{
            SDWebImageDownloader *sself = wself;
            // 5. 取消操作將該url對應(yīng)的回調(diào)信息從URLCallbacks中刪除
            // 阻塞barrierqueue
            if (!sself) return;
            dispatch_barrier_async(sself.barrierQueue, ^{
                [sself.URLCallbacks removeObjectForKey:url];
            });
        }];
        // 是否需要解碼
        operation.shouldDecompressImages = wself.shouldDecompressImages;
        
        if (wself.urlCredential) {
            operation.credential = wself.urlCredential;
        } else if (wself.username && wself.password) {
            operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
        }
        
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }
        
        // NSOperation Queue 增加一個對象
        //4.設(shè)置依賴
        // [operation2 addDependency:operation1];      任務(wù)二依賴任務(wù)一
        // [operation3 addDependency:operation2];      任務(wù)三依賴任務(wù)二
        // 6. 將操作加入到操作隊(duì)列downloadQueue中
        [wself.downloadQueue addOperation:operation];
        // 如果不是FIFO 是 LIFO 隊(duì)列設(shè)置依賴,后進(jìn)來的成為上面的依賴
        if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
            //FIFO的話正常數(shù)組就問題
            // LIFO的話讓之前的操作一次依賴最后一次進(jìn)來的操作就行了
            [wself.lastAddedOperation addDependency:operation];
            wself.lastAddedOperation = operation;
        }
    }];
    
    return operation;
}

知識點(diǎn):

1.通過調(diào)用addProgressCallback:completeBlock:forURL:createBlock:來確保同一url只會下載一次(看下面注釋)

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
    // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return;
    }
    // 1. 以dispatch_barrier_sync操作來保證同一時間只有一個線程能對URLCallbacks進(jìn)行操作
    // 該屬性是一個字典蝶涩,key是圖片的URL地址理朋,value則是一個數(shù)組,包含每個圖片的多組回調(diào)信息绿聘。由于我們允許多個圖片同時下載嗽上,因此可能會有多個線程同時操作URLCallbacks屬性。為了保證URLCallbacks操作(添加熄攘、刪除)的線程安全性兽愤,SDWebImageDownloader將這些操作作為一個個任務(wù)放到barrierQueue隊(duì)列中,并設(shè)置屏障來確保同一時間只有一個線程操作URLCallbacks屬性
    // 這個寫法阻塞當(dāng)前線程  而且阻塞barrierQueue隊(duì)列
    // 這句話表示每個URL下載只會出來一次creatBlock回調(diào)出去創(chuàng)建新的任務(wù)operation(最終要添加到queue的任務(wù))
    dispatch_barrier_sync(self.barrierQueue, ^{
        BOOL first = NO;
        if (!self.URLCallbacks[url]) {
            // 當(dāng)?shù)谝淮芜M(jìn)來下載的時候挪圾,我們平時外部都是傳個completeblock浅萧,所以我們的urlcallbacks結(jié)構(gòu)是{"url":[{"comolete":"completeBlock"}]}
            // 如果是多次重復(fù)下載同一URL圖片,結(jié)構(gòu)應(yīng)該會變成
            // {"url":[{"comolete":"completeBlock"},{"comolete":"completeBlock"},{"comolete":"completeBlock"},{"comolete":"completeBlock"}]}
            self.URLCallbacks[url] = [NSMutableArray new];
            first = YES;
        }
        
        // Handle single download of simultaneous download request for the same URL
        // 2. 處理同一URL的同步下載請求的單個下載
        NSMutableArray *callbacksForURL = self.URLCallbacks[url];
        NSMutableDictionary *callbacks = [NSMutableDictionary new];
        if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
        if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
        [callbacksForURL addObject:callbacks];
        self.URLCallbacks[url] = callbacksForURL;
        
        // 如果是第一次洛史,那么回調(diào)出去下載
        if (first) {
            createCallback();
        }
    });
}

2.通過繼承NSOperation的SDWebImageDownloaderOperation進(jìn)來初始化下載任務(wù)(下一步再講解內(nèi)部)惯殊,這里的回調(diào)就是上面方法里面數(shù)據(jù)結(jié)構(gòu)存儲起來的所有回調(diào)的遍歷執(zhí)行

progressBlock,completeBlock和CancelBlock都用到了GCD的barrier的方法,有時間慢慢再看看原理,先看基本介紹

// 所有下載操作的網(wǎng)絡(luò)響應(yīng)序列化處理是放在一個自定義的并行調(diào)度隊(duì)列中來處理的

// _barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);

@property (SDDispatchQueueSetterSementics,nonatomic)dispatch_queue_t barrierQueue;

//func dispatch_barrier_async(_ queue: dispatch_queue_t, _ block: dispatch_block_t):

//這個方法重點(diǎn)是你傳入的 queue也殖,當(dāng)你傳入的 queue是通過 DISPATCH_QUEUE_CONCURRENT參數(shù)自己創(chuàng)建的 queue 時,這個方法會阻塞這個queue(注意是阻塞 queue,而不是阻塞當(dāng)前線程)忆嗜,一直等到這個 queue中排在它前面的任務(wù)都執(zhí)行完成后才會開始執(zhí)行自己己儒,自己執(zhí)行完畢后,再會取消阻塞捆毫,使這個 queue中排在它后面的任務(wù)繼續(xù)執(zhí)行闪湾。

//如果你傳入的是其他的 queue,那么它就和 dispatch_async一樣了。

//func dispatch_barrier_sync(_ queue: dispatch_queue_t, _ block: dispatch_block_t):

//這個方法的使用和上一個一樣绩卤,傳入自定義的并發(fā)隊(duì)列(DISPATCH_QUEUE_CONCURRENT)途样,它和上一個方法一樣的阻塞 queue,不同的是這個方法還會阻塞當(dāng)前線程濒憋。

//如果你傳入的是其他的 queue,那么它就和 dispatch_sync一樣了何暇。

3.把createBlock里面的網(wǎng)絡(luò)請求任務(wù)加入NSOperationQueue隊(duì)列中,該隊(duì)列右兩個屬性

// 執(zhí)行的下載順序
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
    /**
     * Default value. All download operations will execute in queue style (first-in-first-out).
     * 默認(rèn)是FIFO   以隊(duì)列的方式凛驮,按照先進(jìn)先出的順序下載裆站。這是默認(rèn)的下載順序
     */
    SDWebImageDownloaderFIFOExecutionOrder,
    
    /**
     * All download operations will execute in stack style (last-in-first-out).
     * 以棧的方式,按照后進(jìn)先出的順序下載黔夭。
     */
    SDWebImageDownloaderLIFOExecutionOrder
};

GCD不能很好的設(shè)置依賴關(guān)系,那么NSOperation就能很好的實(shí)現(xiàn)了,關(guān)鍵代碼讓上一次的任務(wù)依賴于最后進(jìn)來的任務(wù)芦鳍,就能實(shí)現(xiàn)LIFO

[wself.lastAddedOperationaddDependency:operation];

5. 第五步(SDWebImageDownloaderOperation)

這個類是繼承與NSOperation的焦辅,并且采用了SDWebImageOperation的代理(只有個cancel的方法),并且它只暴露了一個方法,initWithRequest:inSession:options:progress:completed:canceled這個初始化方法來配置

由于他是自定義的婚惫,那么就必須重寫Start的方法了牛,在該方法里面SD已經(jīng)把NSURLConnection替換成了NSURLSession來進(jìn)行網(wǎng)絡(luò)請求的操作,簡言之辰妙,只要實(shí)現(xiàn)NSURLSession的代理方法就能獲取到下載數(shù)據(jù)

這里主要看下一個不斷接受data的代理回調(diào)

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    
    // 這個方法本身就已經(jīng)是異步了
    //    2016-10-20 16:49:40.260 SDWebImageAnalyze[7098:374046] 我正在接受數(shù)據(jù)<NSThread: 0x608000260140>{number = 3, name = (null)}
    //    2016-10-20 16:49:40.261 SDWebImageAnalyze[7098:374032] 我正在接受數(shù)據(jù)<NSThread: 0x600000265980>{number = 5, name = (null)}
    //    2016-10-20 16:49:40.261 SDWebImageAnalyze[7098:374029] 我正在接受數(shù)據(jù)<NSThread: 0x600000077580>{number = 6, name = (null)}
    // 1. 附加數(shù)據(jù)
    
    [self.imageData appendData:data];
    
    if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
        // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/
        // Thanks to the author @Nyx0uf
        
        // Get the total bytes downloaded
        // 2. 獲取已下載數(shù)據(jù)總大小
        const NSInteger totalSize = self.imageData.length;
        
        // Update the data source, we must pass ALL the data, not just the new bytes
        // 3. 更新數(shù)據(jù)源鹰祸,我們需要傳入所有數(shù)據(jù),而不僅僅是新數(shù)據(jù)
        CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
        
        // 4. 首次獲取到數(shù)據(jù)時密浑,從這些數(shù)據(jù)中獲取圖片的長蛙婴、寬、方向?qū)傩灾?        if (width + height == 0) {
            CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
            if (properties) {
                NSInteger orientationValue = -1;
                CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
                if (val) CFNumberGetValue(val, kCFNumberLongType, &height);
                val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
                if (val) CFNumberGetValue(val, kCFNumberLongType, &width);
                val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
                if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);
                CFRelease(properties);
                
                // When we draw to Core Graphics, we lose orientation information,
                // which means the image below born of initWithCGIImage will be
                // oriented incorrectly sometimes. (Unlike the image born of initWithData
                // in didCompleteWithError.) So save it here and pass it on later.
                // 5. 當(dāng)繪制到Core Graphics時尔破,我們會丟失方向信息街图,這意味著有時候由initWithCGIImage創(chuàng)建的圖片
                //    的方向會不對,所以在這邊我們先保存這個信息并在后面使用懒构。
                orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
            }
            
        }
        // 6. 圖片還未下載完成
        if (width + height > 0 && totalSize < self.expectedSize) {
            // Create the image
            // 7. 使用現(xiàn)有的數(shù)據(jù)創(chuàng)建圖片對象餐济,如果數(shù)據(jù)中存有多張圖片,則取第一張
            CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
            
#ifdef TARGET_OS_IPHONE
            // Workaround for iOS anamorphic image
            // 8. 適用于iOS變形圖像的解決方案胆剧。我的理解是由于iOS只支持RGB顏色空間絮姆,所以在此對下載下來的圖片做個顏色空間轉(zhuǎn)換處理醉冤。
            if (partialImageRef) {
                const size_t partialHeight = CGImageGetHeight(partialImageRef);
                CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
                CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
                CGColorSpaceRelease(colorSpace);
                if (bmContext) {
                    CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef);
                    CGImageRelease(partialImageRef);
                    partialImageRef = CGBitmapContextCreateImage(bmContext);
                    CGContextRelease(bmContext);
                }
                else {
                    CGImageRelease(partialImageRef);
                    partialImageRef = nil;
                }
            }
#endif
            
            // 9. 對圖片進(jìn)行縮放、解碼操作
            if (partialImageRef) {
                UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
                NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                UIImage *scaledImage = [self scaledImageForKey:key image:image];
                if (self.shouldDecompressImages) {
                    image = [UIImage decodedImageWithImage:scaledImage];
                }
                else {
                    image = scaledImage;
                }
                CGImageRelease(partialImageRef);
                
                dispatch_main_sync_safe(^{
                    if (self.completedBlock) {
                        self.completedBlock(image, nil, nil, NO);
                    }
                });
            }
        }
        
        CFRelease(imageSource);
    }
    
    if (self.progressBlock) {
        self.progressBlock(self.imageData.length, self.expectedSize);
    }
}

CG框架下的圖片處理還是有點(diǎn)看的懵逼篙悯,還有圖片縮放以及解壓操作蚁阳,以我目前的知識還是很難理解,不過加了了宏觀的中文注解鸽照,稍微先過下流程

 - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {}  

最終螺捐,complete的代理回調(diào)把圖片一步步回調(diào)出去,就如剛才一步步走進(jìn)來一樣

6. 第六步(回調(diào)到SDWebImageManager存儲圖片矮燎,完成最終回調(diào))

內(nèi)存緩存沒什么好講的定血,直接調(diào)用NSCache的set方法

// 內(nèi)存緩存或者磁盤緩存
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
    if (!image || !key) {
        return;
    }
    // 內(nèi)存緩存有必要的話
    if (self.shouldCacheImagesInMemory) {
        NSUInteger cost = SDCacheCostForImage(image);
        // 1. 內(nèi)存緩存,將其存入NSCache中诞外,同時傳入圖片的消耗值
        [self.memCache setObject:image forKey:key cost:cost];
    }
    // 硬盤緩存
    if (toDisk) {
        // 異步串行隊(duì)列寫入
        // 2. 如果確定需要磁盤緩存澜沟,則將緩存操作作為一個任務(wù)放入ioQueue中
        dispatch_async(self.ioQueue, ^{
            NSData *data = imageData;
            
            if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
                // We need to determine if the image is a PNG or a JPEG
                // PNGs are easier to detect because they have a unique signature (http://www.w3.org/TR/PNG-Structure.html)
                // The first eight bytes of a PNG file always contain the following (decimal) values:
                // 137 80 78 71 13 10 26 10
                
                // If the imageData is nil (i.e. if trying to save a UIImage directly or the image was transformed on download)
                // and the image has an alpha channel, we will consider it PNG to avoid losing the transparency
                int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
                BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                                  alphaInfo == kCGImageAlphaNoneSkipFirst ||
                                  alphaInfo == kCGImageAlphaNoneSkipLast);
                // 判斷圖片格式
                // 3. 需要確定圖片是PNG還是JPEG。PNG圖片容易檢測浅乔,因?yàn)橛幸粋€唯一簽名倔喂。PNG圖像的前8個字節(jié)總是包含以下值:137 80 78 71 13 10 26 10
                // 在imageData為nil的情況下假定圖像為PNG。我們將其當(dāng)作PNG以避免丟失透明度靖苇。而當(dāng)有圖片數(shù)據(jù)時席噩,我們檢測其前綴,確定圖片的類型
                BOOL imageIsPng = hasAlpha;
                
                // But if we have an image data, we will look at the preffix
                // 查看imagedata的的前綴是否是png的
                if ([imageData length] >= [kPNGSignatureData length]) {
                    imageIsPng = ImageDataHasPNGPreffix(imageData);
                }
                
                if (imageIsPng) {
                    data = UIImagePNGRepresentation(image);
                }
                else {
                    data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
                }
#else
                data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
            }
            
            [self storeImageDataToDisk:data forKey:key];
        });
    }
}

我們來看看磁盤緩存

// 硬盤緩存方法
- (void)storeImageDataToDisk:(NSData *)imageData forKey:(NSString *)key {
    
    if (!imageData) {
        return;
    }
    // 是否根據(jù)路徑存在文件 不存在就創(chuàng)建
    // 創(chuàng)建緩存文件并存儲圖片
    // 根據(jù) // library/caches/default/com.hackemist.SDWebImageCache.default創(chuàng)建了文件夾
    if (![_fileManager fileExistsAtPath:_diskCachePath]) {
        [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
    }
    // get cache Path for image key
    // /Users/mkjing/Library/Developer/CoreSimulator/Devices/7A62E354-CB88-4012-A119-7B64089B7171/data/Containers/Data/Application/E5275526-CD16-499D-B731-6D68938C04FB/Library/Caches/default/com.hackemist.SDWebImageCache.default
    // 該路徑下面拼接圖片的url路徑
    // key是經(jīng)過MD5加密的字符串
    NSString *cachePathForKey = [self defaultCachePathForKey:key];
    // transform to NSUrl
    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
    // 保存文件到指定路徑中
    //  // library/caches/default/com.hackemist.SDWebImageCache.default/文件名
    [_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
    
    // disable iCloud backup
    if (self.shouldDisableiCloud) {
        [fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
    }
}

7.第七步(SDWebImageCache的清理緩存策略)

在初始化的時候注冊了幾個通知

// 收到內(nèi)存警告贤壁,清楚NSCache [self.memCache removeAllObject]
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(clearMemory)
                                             name:UIApplicationDidReceiveMemoryWarningNotification
                                           object:nil];

// 程序關(guān)閉時對硬盤文件做一些處理
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(cleanDisk)
                                             name:UIApplicationWillTerminateNotification
                                           object:nil];

// 程序進(jìn)入后臺是也對硬盤進(jìn)行一些讀寫
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(backgroundCleanDisk)
                                             name:UIApplicationDidEnterBackgroundNotification
                                           object:nil];

1.當(dāng)收到內(nèi)存警告時悼枢,直接調(diào)用NSCache的removeAllObject的方法來清理MemeryCache
2.當(dāng)程序退出時或進(jìn)入后臺,根據(jù)緩存策略來清理磁盤緩存

// 當(dāng)程序退出或者推到后臺的時候脾拆,會有緩存策略管理
// 接收到程序進(jìn)入后臺或者程序退出通知
// 調(diào)用該方法馒索,然后先遍歷所有緩存文件,記錄過期的文件名船,計(jì)算緩存總文件大小
// 先刪除過期的文件(默認(rèn)一周)
// 如果設(shè)置最大緩存绰上,而且已經(jīng)緩存的文件大小超過這個預(yù)期值,把所有的文件按最后編輯的時間升序渠驼,然后一個個刪除蜈块,當(dāng)緩存低于臨界就break
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
   dispatch_async(self.ioQueue, ^{
       // diskCachePath
       //        /Users/mkjing/Library/Developer/CoreSimulator/Devices/7A62E354-CB88-4012-A119-7B64089B7171/data/Containers/Data/Application/E5275526-CD16-499D-B731-6D68938C04FB/Library/Caches/default/com.hackemist.SDWebImageCache.default
       NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
       // 需要獲取的屬性列表 是否文件夾  最后一次編輯時間和文件大小(如有壓縮迷扇,就是壓縮后的)
       NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
       
       // This enumerator prefetches useful properties for our cache files.
       // 1. 該枚舉器預(yù)先獲取緩存文件的有用的屬性 (根據(jù)存儲的文件夾獲取所有文件的枚舉器)
       NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                  includingPropertiesForKeys:resourceKeys
                                                                     options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                errorHandler:NULL];
       // 當(dāng)前時間  2016-10-21 02:46:41 +0000
       // expirationDate  2016-10-14 02:46:11 +0000
       NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
       NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
       NSUInteger currentCacheSize = 0;
       
       // Enumerate all of the files in the cache directory.  This loop has two purposes:
       //
       //  1. Removing files that are older than the expiration date.
       //  2. Storing file attributes for the size-based cleanup pass.
       // 2. 枚舉緩存文件夾中所有文件百揭,該迭代有兩個目的:移除比過期日期更老的文件;存儲文件屬性以備后面執(zhí)行基于緩存大小的清理操作
       NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
       for (NSURL *fileURL in fileEnumerator) {
           // 根據(jù)resourceKeys和路徑獲取到遍歷文件的數(shù)據(jù)字典
           NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
           
           // Skip directories.
           // 3. 跳過文件夾
           if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
               continue;
           }
           
           // Remove files that are older than the expiration date;
           // 4. 移除早于有效期的老文件 (根據(jù)最后一次編輯時間屬性來判斷)
           NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];// 獲取文件編輯時間
           // 返回晚一點(diǎn)的date 如果是experiationDate 蜓席,說明該文件是要刪除的
           if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
               [urlsToDelete addObject:fileURL];
               continue;
           }
           
           // Store a reference to this file and account for its total size.
           // 5. 存儲文件的引用并計(jì)算所有文件的總大小器一,以備后用
           NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
           currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
           // 把所有的文件都裝進(jìn)cachefile里面 根據(jù)fileURL定位
           [cacheFiles setObject:resourceValues forKey:fileURL];
       }
       // 移除過期的先
       for (NSURL *fileURL in urlsToDelete) {
           [_fileManager removeItemAtURL:fileURL error:nil];
       }
       
       // If our remaining disk cache exceeds a configured maximum size, perform a second
       // size-based cleanup pass.  We delete the oldest files first.
       // 6.如果磁盤緩存的大小大于我們配置的最大大小,則執(zhí)行基于文件大小的清理厨内,我們首先刪除最老的文件
       if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
           // Target half of our maximum cache size for this cleanup pass.
           // 7. 以設(shè)置的最大緩存大小的一半作為清理目標(biāo)
           const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
           
           // Sort the remaining cache files by their last modification time (oldest first).
           // 從小到大排序祈秕,也就是最早的時間在最前面 升序// 8. 按照最后修改時間來排序剩下的緩存文件
           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.
           // 9. 刪除文件渺贤,直到緩存總大小降到我們期望的大小
           for (NSURL *fileURL in sortedFiles) {
               // 由于之前過期的一部分已經(jīng)移除了,但是都加進(jìn)了cacheFile里面踢步,如果不能移除癣亚,我們已經(jīng)過期刪除了丑掺,直接跳過進(jìn)行下一個
               // 移除成功获印,那么計(jì)算cacheFile大小
               if ([_fileManager removeItemAtURL:fileURL error:nil]) {
                   NSDictionary *resourceValues = cacheFiles[fileURL];
                   NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                   currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];
                   // 降到期望值以下就可以停了
                   if (currentCacheSize < desiredCacheSize) {
                       break;
                   }
               }
           }
       }
       if (completionBlock) {
           dispatch_async(dispatch_get_main_queue(), ^{
               completionBlock();
           });
       }
   });
}

3.穿插一個clearDisk的方法,這個就不是什么策略了街州,直接是完全清掉磁盤緩存 clear 和 clean的區(qū)別吧

// 清理磁盤 (完全清理)
// clear清理是通過刪除路徑文件夾兼丰,然后再創(chuàng)建的方式進(jìn)行
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion
{
    // 異步自定義串行隊(duì)列
    dispatch_async(self.ioQueue, ^{
        // 直接把文件夾移除
        [_fileManager removeItemAtPath:self.diskCachePath error:nil];
        [_fileManager createDirectoryAtPath:self.diskCachePath
                withIntermediateDirectories:YES
                                 attributes:nil
                                      error:NULL];
        
        if (completion) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completion();
            });
        }
    });
}

那么到這里,基本的流程和主要技術(shù)點(diǎn)就走完了唆缴,我覺得已經(jīng)注釋的很詳細(xì)了鳍征,還是整理下知識點(diǎn)給大家
知識點(diǎn)整理:

1.這里的Dispatch_barrier_async來確保線程安全操作,任務(wù)需要等待之前的任務(wù)執(zhí)行完
2.強(qiáng)大的Block回調(diào)和Associated Objects — runtime
3.NSOperationQueue和NSOperation的面徽,相對于GCD來說艳丛,他能取消操作,也能設(shè)置任務(wù)之間的依賴趟紊,相較于GCD來說更加的強(qiáng)大,AF也是基于這個玩的氮双,這樣才可以完成LIFO的隊(duì)列需求
4.NSCache和NSFileManager的二級緩存操作
5.緩存策略:超過一星期的殺掉,還能設(shè)置最大緩存數(shù)霎匈,根據(jù)文件的最后編輯時間進(jìn)行升序戴差,然后一個個刪除,低于臨界的時候清理完成
6.異步操作圖片的處理铛嘱,縮放和解壓操作暖释,還有圖片的類型處理,這東西有空再研究墨吓,很少接觸
7.還有就是同一URL不會被下載的優(yōu)化處理
8.最后還是覺得封裝和任務(wù)的分配都非常的清晰球匕,值得學(xué)習(xí)

流程整理:

1.入口UIImageView調(diào)用方法sd_setImageWithURL: placeholderImage: options: progress:completed:,無論你調(diào)用哪個帖烘,最終都轉(zhuǎn)換成該方法處理url
2.必須先刪除該控件之前的下載任務(wù)亮曹,sd_cancelCurrentImageLoad原因是當(dāng)你網(wǎng)絡(luò)不快的情況下,例如你一個屏幕能展示三個cell蚓让,第一個cell由于網(wǎng)絡(luò)問題不能立刻下完乾忱,那么用戶就滑動了tbv,第一個cell進(jìn)去復(fù)用池历极,第五個出來的cell從復(fù)用池子拿窄瘟,由于之前的下載還在,本來是應(yīng)該顯示第五個圖片趟卸,但是SD的默認(rèn)做法是立馬把下載好的圖片給UIImageView蹄葱,所以這時候會圖片數(shù)據(jù)錯亂氏义,BUG
3.有placeHolder先展示,然后啟用SDWebImageManager單例downloadImageWithURLoptions:progress:completed:來處理圖片下載
4.先判斷url的合法性图云,再創(chuàng)建SDwebImageCombinedOperation的cache任務(wù)對象惯悠,再查看url是否之前下載失敗過,最后如果url為nil竣况,則直接返回操作對象完成回調(diào)克婶,如果都正常,那么就調(diào)用SDWebImageManager中的管理緩存類SDImageCache單例的方法queryDiskCacheForKey:done:查看是否有緩存
5.SDImageCache內(nèi)存緩存用的是NSCache丹泉,Disk緩存用的是NSFileManager的文件寫入操作情萤,那么查看緩存的時候是先去內(nèi)存查找,這里的key都是經(jīng)過MD5之后的字串摹恨,找到直接回調(diào)筋岛,沒找到繼續(xù)去磁盤查找,開異步串行隊(duì)列去找晒哄,避免卡死主線程睁宰,啟用autoreleasepool避免內(nèi)存暴漲,查到了緩存到內(nèi)存寝凌,然后回調(diào)
6.如果都沒找到柒傻,就調(diào)用SDWebImageManager中的管理下載類SDWebImageDownloader單例
downloadImageWithURL:options:progress:completed:completedBlock處理下載
7.下載前調(diào)用addProgressCallback:completedBlock:forURL:createCallback:來保證統(tǒng)一url只會生成一個網(wǎng)絡(luò)下載對象,多余的都只會用URLCallbacks存儲傳入的進(jìn)度Block或者CompleteBlock硫兰,因此下載結(jié)果返回的時候會進(jìn)行遍歷回調(diào)
8.下載用NSOperation和NSOperationQueue來進(jìn)行诅愚,SD派生了一個SDWebImageDownloaderOperation負(fù)責(zé)圖片的下載任務(wù),調(diào)用
initWithRequest:inSession:options:progress:completed:cancelled:
9.把返回的SDWebImageDownloaderOperation對象add到NSOperationQueue劫映,F(xiàn)IFO隊(duì)列就正常违孝,如果是LIFO隊(duì)列,就需要設(shè)置依賴泳赋,這也是GCD和NSOperation的區(qū)別雌桑,也是NSOperation的優(yōu)點(diǎn),讓上一次的任務(wù)依賴于本次任務(wù)[wself.lastAddedOperationaddDependency:operation]
10.下載任務(wù)開始是用NSURLSession了祖今,不用NSURLConnetion了校坑,由于SD是自定義的NSOperation
內(nèi)部需要重寫start方法,在該方法里面配置Session千诬,當(dāng)taskResume的時候耍目,根據(jù)設(shè)置的代理就能取到不同的回調(diào)參數(shù)
didReceiveResponse能獲取到響應(yīng)的所有參數(shù)規(guī)格,例如總size
didReceiveData是一步步獲取data徐绑,壓縮解碼回調(diào)progressBlock
didCompleteWithError全部完成回調(diào)邪驮,圖片解碼,回調(diào)completeBlock
11.圖片的解碼是在SDWebImageDecoder里面完成的傲茄,縮放操作是在SDWebImageCompat內(nèi)完成的毅访,代理方法里面本身就已經(jīng)是異步了沮榜,而且解碼操作加入了autoreleasepool減少內(nèi)存峰值
這方面知識還是需要進(jìn)一步去了解,不是圖片壓縮解碼不是很懂
12.當(dāng)在SDWebImageDownloaderOperation中NSURLSession完成下載之后或者中途回調(diào)到SDWebImageDownloader中喻粹,然后再回調(diào)到SDWebImageManager蟆融,在Manager中二級緩存image,然后繼續(xù)回調(diào)出去到UIImage + WebCache中守呜,最后把Image回調(diào)出去型酥,在調(diào)用的控件中展示出來
13.SDImageCache初始化的時候注冊了幾個通知,當(dāng)內(nèi)存警告的時候弛饭,程序進(jìn)入后臺或者程序殺死的時候根據(jù)策略清理緩存
內(nèi)存警告:自動清除NSCache內(nèi)存緩存
進(jìn)入后臺和程序殺死:清理過期的文件(默認(rèn)一周),然后有個緩存期望值冕末,對比已有文件的大小萍歉,先根據(jù)文件最后編輯時間升序排侣颂,把大于期望值大小的文件全部殺掉
14.SDWebImagePrefetcher這貨還提供了預(yù)先下載圖片,還沒使用過

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末枪孩,一起剝皮案震驚了整個濱河市憔晒,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蔑舞,老刑警劉巖拒担,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異攻询,居然都是意外死亡从撼,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進(jìn)店門钧栖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來低零,“玉大人,你說我怎么就攤上這事拯杠√蜕簦” “怎么了?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵潭陪,是天一觀的道長雄妥。 經(jīng)常有香客問我,道長依溯,這世上最難降的妖魔是什么老厌? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮黎炉,結(jié)果婚禮上枝秤,老公的妹妹穿的比我還像新娘。我一直安慰自己拜隧,他們只是感情好宿百,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布趁仙。 她就那樣靜靜地躺著,像睡著了一般垦页。 火紅的嫁衣襯著肌膚如雪雀费。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天痊焊,我揣著相機(jī)與錄音盏袄,去河邊找鬼。 笑死薄啥,一個胖子當(dāng)著我的面吹牛辕羽,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播垄惧,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼刁愿,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了到逊?” 一聲冷哼從身側(cè)響起铣口,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎觉壶,沒想到半個月后脑题,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡铜靶,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年叔遂,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片争剿。...
    茶點(diǎn)故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡已艰,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出秒梅,到底是詐尸還是另有隱情旗芬,我是刑警寧澤,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布捆蜀,位于F島的核電站疮丛,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏辆它。R本人自食惡果不足惜誊薄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望锰茉。 院中可真熱鬧呢蔫,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至俏脊,卻和暖如春全谤,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背爷贫。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工认然, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人漫萄。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓卷员,卻偏偏與公主長得像,于是被迫代替她去往敵國和親腾务。 傳聞我的和親對象是個殘疾皇子毕骡,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評論 2 350