SDWebImageManager 實現(xiàn)分析

  • UIButton+WebCache
  • UIImageView+WebCache
  • MKAnnotationView+WebCache

等Category 都是直接調(diào)用 SDWebImageManager 的方法
SDWebImageManager 內(nèi)部使用 Downloader 和 Cache 來協(xié)調(diào)下載和緩存的任務.

SDWebImageManager分析

毫無疑問 SDWebImageManager 有這2個屬性
**@property (strong, nonatomic, readonly) SDImageCache imageCache;
@property (strong, nonatomic, readonly) SDWebImageDownloader imageDownloader;

SDWebImageManager 這些顧名思義的方法也都是直接調(diào)用 SDImageCache的方法來實現(xiàn)的.

- (void)saveImageToCache:(UIImage *)image forURL:(NSURL *)url;
- (BOOL)cachedImageExistsForURL:(NSURL *)url;
- (void)diskImageExistsForURL:(NSURL *)url
                   completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
... ...

具體實現(xiàn)

大部分的緩存和下載工作都在這一個方法中完成

代碼太長 ~ 無關代碼有省略

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {

    1.如果你將 url 參數(shù)傳成了字符串,我們幫你轉(zhuǎn)成 NSURL
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    2.防止你亂傳參數(shù),console 輸出奇怪的錯誤信息
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }

    __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    __weak SDWebImageCombinedOperation *weakOperation = operation;

    BOOL isFailedUrl = NO;
    @synchronized (self.failedURLs) {
        isFailedUrl = [self.failedURLs containsObject:url];
    }
    
    3.如果url 不正確,或者此 url 已經(jīng)被下載過而且失敗了, 也不需要重試,那么就取消下載,直接回調(diào)下載完成 block, 返回錯誤信息,
    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        dispatch_main_sync_safe(^{
            NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
            completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
        });
        return operation;
    }

    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }
    NSString *key = [self cacheKeyForURL:url];
    
    4.先查詢緩存中是否存在
    operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
    
    5.關于 cancel 下載操作,沒有辦法強制中斷正在執(zhí)行的 Operation, 當然你可以主動殺死 App..  
    一般取消的操作都是設置一個標記 flag, 在執(zhí)行重要,耗時的操作之前檢查這個標記,如果已經(jīng)被取消了,就不繼續(xù)執(zhí)行.
        if (operation.isCancelled) {
            @synchronized (self.runningOperations) {
                [self.runningOperations removeObject:operation];
            }
            return;
        }
        
        6.詢問代理是否應該下載這個 url 對應的圖片
        if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
            if (image && options & SDWebImageRefreshCached) {
                dispatch_main_sync_safe(^{
                7.如果磁盤緩存中存在,直接回調(diào)下載完成 block 但是不中斷下載任務,嘗試,重新下載圖片,為了讓 NSURLCache 刷新緩存狀態(tài)
                因為一個 url 對應的圖片可能會變化,比如 url 對應一個用戶的頭像,而這個頭像用戶隨時可能更改
                    completedBlock(image, nil, cacheType, YES, url);
                });
            }
            
            7.終于開始準備下載圖片了, 將SDWebImageOptions 和 SDWebImageDownloaderOptions 做一些協(xié)調(diào)轉(zhuǎn)換的工作.
            SDWebImageDownloaderOptions downloaderOptions = 0;
            if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
            if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
            if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
            if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
            if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
            if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
            if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
            if (image && options & SDWebImageRefreshCached) {
            
                8.如果磁盤緩存中有圖片,就關閉 progressive 下載方式(圖片會從上到下,下載一部分,顯示一部分)  
                downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;  
                9.如果磁盤緩存中有圖片,讓 NSURLCache 刷新緩存狀態(tài)  
                downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;  
            }
            
            10.準備完成..正式調(diào)用下載工作
            id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                if (!strongOperation || strongOperation.isCancelled) {
                //什么都沒有, Github issues 的 bugfix
                }
                else if (error) {
                
                11.出錯了或者被取消了就回調(diào)下載完成的 block
                    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) {
                        
                        12.如果因為如上原因下載失敗,就加入self.failedURLs 黑名單,如果沒設置下載失敗重試,下次就不下載了
                        @synchronized (self.failedURLs) {
                            [self.failedURLs addObject:url];
                        }
                    }
                }
                else {
                    if ((options & SDWebImageRetryFailed)) {
                    
                        13.如果設置了下載失敗重試,就不加入黑名單,每次都重新下載圖片,不管上次是否下載失敗
                        @synchronized (self.failedURLs) {
                            [self.failedURLs removeObject:url];
                        }
                    }
                    
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

                    if (options & SDWebImageRefreshCached && image && !downloadedImage) {
                        14.如果這次下載是為了讓 NSURLCache 刷新緩存狀態(tài) 就不調(diào)用回調(diào)block
                    }
                    else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
                    
                        15.詢問代理是否要在image 存儲到緩存之前做一些最后的操作,(縮放,裁剪,圓角等)
                        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];
                                
                                16.存入內(nèi)存和磁盤緩存
                                [self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
                            }
                            
                            17.回調(diào)主線程,圖片終于下載完了,而且不是從緩存中取出來的...
                            dispatch_main_sync_safe(^{
                                if (strongOperation && !strongOperation.isCancelled) {
                                    completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
                                }
                            });
                        });
                    }
                    else {
                    18.如果沒有實現(xiàn)代理,直接把圖片存入緩存
                        if (downloadedImage && finished) {
                            [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
                        }
                        19,然后回調(diào)主線程,和17一樣...
                        dispatch_main_sync_safe(^{
                            if (strongOperation && !strongOperation.isCancelled) {
                                completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
                            }
                        });
                    }
                }
                
                19.將下載完成的 Operation 從runningOperations數(shù)組中移除
                SDWebImageManager的 isRunning 方法的實現(xiàn)是判斷 self.runningOperations
                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) {
        20.這怎么還有個完成的回調(diào)..其實些 block 回調(diào)嵌套的有點惡心..
        這個回調(diào)是在磁盤或者內(nèi)存緩存中查詢到圖片時的回調(diào),此時 image 為緩存中的數(shù)據(jù), cacheType為 SDImageCacheTypeDisk或 SDImageCacheTypeMemory
            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 {
            21.這里的 else 是,如果緩存中沒有,并且代理不允許下載這個 url 對應的圖片,會執(zhí)行下面的回調(diào)
            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;
}

這個雖然長但是并不難理解的下載緩存過程終于分析完了... SDWebImage 的核心我們也就理解了

SDWebImageOptions

這個 Options 類似第二篇中的 SDWebImageDownloaderOptions

在 UIImageView 等的 Category,或者直接調(diào)用SDWebImageManager 下載都能自定義SDWebImageOptions設置來完成更多自定義的操作

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {

    //1.每次都重新嘗試下載圖片
    //下載失敗后不將 圖片的 url 加入黑名單,每次都重新下載圖片
    //如果加入了黑名單,下次再請求這個 url 時直接返回下載失敗,不嘗試下載,節(jié)省資源
    SDWebImageRetryFailed = 1 << 0,


    //2.下載圖片優(yōu)先級低
    //默認情況下,有 UI 事件發(fā)生時,比如點擊按鈕, tableview 滾動,下載任務也會同時在其他線程異步執(zhí)行,并不會阻塞主線程,但下載會消耗 cpu, 可能會造成卡頓.
    //設置這個LowPriority 后,只有 tableview 不滾動時才會下載.
    SDWebImageLowPriority = 1 << 1,

    //3.只啟用內(nèi)存緩存,可以用它實現(xiàn)隱私瀏覽?
    SDWebImageCacheMemoryOnly = 1 << 2,

    //4.圖片會從上到下,下載一些顯示一些,網(wǎng)速慢的時候,優(yōu)化體驗,默認不開啟
    SDWebImageProgressiveDownload = 1 << 3,


    //5.即使存在圖片緩存,也嘗試下載操作, 因為同一個 url 對應的圖片可能會變化
    //例如用戶的頭像,用戶可以隨時上傳更新頭像,那我們就必須嘗試下載更新這個圖片,如果更新操作成功,會調(diào)用 下載完成的 completion Block
    SDWebImageRefreshCached = 1 << 4,

    //5.如果App進入后臺,啟用這個參數(shù)會在向系統(tǒng)要求額外的時間來將下載圖片隊列中的下載請求執(zhí)行完畢 
    //如果額外的下載時間過長可能會被系統(tǒng)主動取消下載操作
    SDWebImageContinueInBackground = 1 << 5,

    //6.設置 NSMutableURLRequest.HTTPShouldHandleCookies = YES; 處理Cookie的存儲
    SDWebImageHandleCookies = 1 << 6,

    //7.允許不安全的SSL傳輸,如果后臺配置了https,測試階段可以加這個參數(shù),Release時取消這參數(shù)
    SDWebImageAllowInvalidSSLCertificates = 1 << 7,

    //8.直接將這個圖片下載任務放到下載隊列的頭,讓這個下載任務先被執(zhí)行
    SDWebImageHighPriority = 1 << 8,
    
    //9.延遲設置 PlaceHolder 圖片,當圖片下載完時才會設置 PlaceHolder, 那么默認情況下,ImageView 不會顯示任何內(nèi)容,只會顯示其背景色.
    SDWebImageDelayPlaceholder = 1 << 9,

    //10.默認情況下,如果圖片是 Gif ,不會調(diào)用代理方法 transformDownloadedImage 執(zhí)行對圖片的自定義操作,(關于代理方法下面一點點就會提到),設置這個 flag 對 Gif 也調(diào)用代理方法
    SDWebImageTransformAnimatedImage = 1 << 10,
    
    //11.默認情況下,圖片下載完成后就通過 imageView.image=image 被設置給 imageView,我們可以阻止這一行為,然后在下載完成回調(diào)方法中先處理圖片,加圓角,加濾鏡等,之后再手動設置給
    imageView
    SDWebImageAvoidAutoSetImage = 1 << 11
};

我們可以發(fā)現(xiàn)這有很多 Option和 SDWebImageDownloaderOptions 類似,因為在上面的具體實現(xiàn)中,就是將SDWebImageOptions和SDWebImageDownloaderOptions 做了一個轉(zhuǎn)換或者說傳遞的工作
比如SDWebImageProgressiveDownload, SDWebImageContinueInBackground 等都是傳遞給它的下載模塊來執(zhí)行的.

P.S. 因為它是 NS_OPTIONS 所以我們可以同時設置多個 Option
類似 : SDWebImageRetryFailed | SDWebImageContinueInBackground

還有個 SDWebImageManagerDelegate

@protocol SDWebImageManagerDelegate <NSObject>
@optional

 可以控制當緩存中沒有這個 url 對應的圖片時,是否應該下載它,默認Yes 會下載
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;

 允許下載,完成圖片之后,放入緩存之前,做最后的操作,裁剪,圓角等
 注意,這個方法是在 get_global_queue 中執(zhí)行的,不能調(diào)用設置 UI 的方法.
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;

@end

UIKit 的 Category

UIButton+WebCache
UIImageView+WebCache
MKAnnotationView+WebCache
這些的實現(xiàn)都很簡單,都是調(diào)用 SDWebImageManager 來實現(xiàn)的..

SDWebImagePrefetcher

這個類能以低優(yōu)先級預下載一些圖片,以供后續(xù)的使用,提升用戶體驗.

以SDWebImageLowPriority 預下載,會在系統(tǒng)閑置時執(zhí)行,不會影響主線程和 cpu的效率

主要的方法就一個

- (void)prefetchURLs:(NSArray *)urls progress:(SDWebImagePrefetcherProgressBlock)progressBlock completed:(SDWebImagePrefetcherCompletionBlock)completionBlock;

也可以設置

NSUInteger maxConcurrentDownloads //最大同時下載的圖片數(shù)量
SDWebImageOptions options  

內(nèi)部實現(xiàn)也是調(diào)用 SDWebImageManager的方法,不在贅述


補充

UIImage+GIF

這個分類可以讓UIImage 支持 Gif 圖片

Gif 的本質(zhì)是一張張的圖片,每張展示一小段時間,連續(xù)的切換這些圖片,看起來就是一張動圖了..

+ (UIImage *)sd_animatedGIFWithData:(NSData *)data {
    if (!data) {
        return nil;
    }
    
    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
    
    1.獲取 Gif 包含的真正圖片數(shù)量
    size_t count = CGImageSourceGetCount(source);

    UIImage *animatedImage;
 
    if (count <= 1) {
        animatedImage = [[UIImage alloc] initWithData:data];
    }
    else {
        NSMutableArray *images = [NSMutableArray array];

        NSTimeInterval duration = 0.0f;

        for (size_t i = 0; i < count; i++) {
            CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL);
            if (!image) {
                continue;
            }
            2.獲取每一張圖片,累加他們的播放時間,計算總的 Gif 播放時間
            duration += [self sd_frameDurationAtIndex:i source:source];
            
            3.將每一張圖片存入數(shù)組中
            [images addObject:[UIImage imageWithCGImage:image scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp]];

            CGImageRelease(image);
        }

        if (!duration) {
            duration = (1.0f / 10.0f) * count;
        }
        
        4.根據(jù)總時長創(chuàng)建 UIImage 的 frame 動畫
        animatedImage = [UIImage animatedImageWithImages:images duration:duration];
    }

    CFRelease(source);

    return animatedImage;
}

關于 Category 中的這段代碼

objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

不在本文章的范圍內(nèi),如果你感興趣,可以搜索關鍵字 Assiciate Object
或者看這篇不錯的文章如何在 Category 中為類動態(tài)添加屬性

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末修壕,一起剝皮案震驚了整個濱河市溶褪,隨后出現(xiàn)的幾起案子矾柜,更是在濱河造成了極大的恐慌翎承,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,366評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件管搪,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機行施,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來魂那,“玉大人蛾号,你說我怎么就攤上這事⊙难牛” “怎么了鲜结?”我有些...
    開封第一講書人閱讀 165,689評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長活逆。 經(jīng)常有香客問我精刷,道長,這世上最難降的妖魔是什么蔗候? 我笑而不...
    開封第一講書人閱讀 58,925評論 1 295
  • 正文 為了忘掉前任怒允,我火速辦了婚禮,結果婚禮上锈遥,老公的妹妹穿的比我還像新娘纫事。我一直安慰自己,他們只是感情好所灸,可當我...
    茶點故事閱讀 67,942評論 6 392
  • 文/花漫 我一把揭開白布丽惶。 她就那樣靜靜地躺著,像睡著了一般庆寺。 火紅的嫁衣襯著肌膚如雪蚊夫。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,727評論 1 305
  • 那天懦尝,我揣著相機與錄音知纷,去河邊找鬼壤圃。 笑死,一個胖子當著我的面吹牛琅轧,可吹牛的內(nèi)容都是我干的伍绳。 我是一名探鬼主播,決...
    沈念sama閱讀 40,447評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼乍桂,長吁一口氣:“原來是場噩夢啊……” “哼冲杀!你這毒婦竟也來了?” 一聲冷哼從身側響起睹酌,我...
    開封第一講書人閱讀 39,349評論 0 276
  • 序言:老撾萬榮一對情侶失蹤权谁,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后憋沿,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體旺芽,經(jīng)...
    沈念sama閱讀 45,820評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,990評論 3 337
  • 正文 我和宋清朗相戀三年辐啄,在試婚紗的時候發(fā)現(xiàn)自己被綠了采章。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,127評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡壶辜,死狀恐怖悯舟,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情砸民,我是刑警寧澤抵怎,帶...
    沈念sama閱讀 35,812評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站阱洪,受9級特大地震影響便贵,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜冗荸,卻給世界環(huán)境...
    茶點故事閱讀 41,471評論 3 331
  • 文/蒙蒙 一承璃、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蚌本,春花似錦盔粹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至嵌莉,卻和暖如春进萄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評論 1 272
  • 我被黑心中介騙來泰國打工中鼠, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留可婶,地道東北人。 一個月前我還...
    沈念sama閱讀 48,388評論 3 373
  • 正文 我出身青樓援雇,卻偏偏與公主長得像矛渴,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子惫搏,可洞房花燭夜當晚...
    茶點故事閱讀 45,066評論 2 355

推薦閱讀更多精彩內(nèi)容