SDWebImage(v3.7)源碼剖析

SDWebImage是現(xiàn)在最好用也是用的最廣泛的網(wǎng)絡(luò)圖片下載第三方庫(kù),內(nèi)部封裝很好,非常值得學(xué)習(xí),現(xiàn)整理如下。文中有部分內(nèi)容來自于作者 Haley_WongSDWebImageV3.7.5源碼解析踩娘。

代碼詳解

SDWebImage通過添加category的方式,為UIImageView、UIButton擴(kuò)展設(shè)置網(wǎng)絡(luò)圖片的方法养渴。以UIImageView舉例雷绢,其包含如下設(shè)置圖片的方法:

- (void)sd_setImageWithURL:(NSURL *)url;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options;
- (void)sd_setImageWithURL:(NSURL *)url completed:(SDWebImageCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder completed:(SDWebImageCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options completed:(SDWebImageCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;
- (void)sd_setImageWithPreviousCachedImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;
- (void)sd_setAnimationImagesWithURLs:(NSArray *)arrayOfURLs;

如上這些方法,最終都是調(diào)用各category中的方法:

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

每個(gè)category中的該方法具體邏輯不盡相同理卑,這里以UIImageView為例翘紊,其內(nèi)部的邏輯大體為:

1.取消該UIImageView的當(dāng)前圖片加載操作。(重點(diǎn)一
2.利用runtime的關(guān)聯(lián)對(duì)象AssociatedObject為該UIImageView設(shè)置網(wǎng)絡(luò)圖片的url藐唠,可以通過方法- (NSURL *)sd_imageURL;來獲取該對(duì)象的URL帆疟。(runtime的使用場(chǎng)景)
3.設(shè)置默認(rèn)圖片。(即placeholder宇立,若設(shè)置了延遲設(shè)置placeholder踪宠,則跳過該步)
4.判斷url是否存在,不存在則回調(diào)completedBlock妈嘹,返回錯(cuò)誤信息柳琢;若存在,執(zhí)行下一步润脸。
5.判斷是否添加ActivityIndicatorView柬脸。
6.調(diào)用SDWebImageManager,創(chuàng)建下載圖片的operation毙驯。(重點(diǎn)二倒堕,SDWebImage的核心內(nèi)容
7.為該UIImageView設(shè)置下載的operation。(同樣是通過runtime的關(guān)聯(lián)對(duì)象AssociatedObject實(shí)現(xiàn)
8.執(zhí)行下載完成的completedBlock回調(diào)爆价。(這一步也值得詳細(xì)解析)

重點(diǎn)一

取消UIImageView的當(dāng)前圖片加載操作涩馆。為什么需要取消當(dāng)前加載操作呢?假如某一對(duì)象(UIImageView *)設(shè)置了多個(gè)網(wǎng)絡(luò)圖片時(shí)允坚,因?yàn)橄螺d過程是異步的,假如不取消前一下載操作蛾号,那么最終得到的圖片很可能是錯(cuò)誤的稠项。且浪費(fèi)流量及資源。

- (void)sd_cancelCurrentImageLoad {
    [self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"];
}

- (void)sd_cancelCurrentAnimationImagesLoad {
    [self sd_cancelImageLoadOperationWithKey:@"UIImageViewAnimationImages"];
}

可以看到鲜结,這里有兩個(gè)不同的取消方法展运,因?yàn)閁IImageView除了可以設(shè)置單張圖片,還可以設(shè)置多張網(wǎng)絡(luò)圖片展示動(dòng)畫效果精刷。這兩個(gè)方法內(nèi)部調(diào)用的是同一個(gè)方法:

- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
    // Cancel in progress downloader from queue
    NSMutableDictionary *operationDictionary = [self operationDictionary];
    id operations = [operationDictionary objectForKey:key];
    if (operations) {
        if ([operations isKindOfClass:[NSArray class]]) {
            for (id <SDWebImageOperation> operation in operations) {
                if (operation) {
                    [operation cancel];
                }
            }
        } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
            [(id<SDWebImageOperation>) operations cancel];
        }
        [operationDictionary removeObjectForKey:key];
    }
}

上面這個(gè)方法是UIView的一個(gè)category方法拗胜,在UIView + WebCacheOperation中。其中[self operationDictionary]利用runtime關(guān)聯(lián)對(duì)象(AssociatedObject)獲取當(dāng)前視圖的operationDictionary怒允,沒有的話就創(chuàng)建一個(gè)set上去埂软,具體實(shí)現(xiàn)如下:

- (NSMutableDictionary *)operationDictionary {
    //獲取關(guān)聯(lián)對(duì)象
    NSMutableDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
    if (operations) {
        return operations;
    }
    operations = [NSMutableDictionary dictionary];
    //設(shè)置關(guān)聯(lián)對(duì)象
    objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return operations;
}

后面幾行內(nèi)容就是取消掉當(dāng)前operation的下載操作。因?yàn)榭赡苁荱IImageView的動(dòng)畫圖片纫事,所以就去數(shù)組中一個(gè)個(gè)的取消勘畔。如果是SDWebImage自定義的對(duì)象肯定會(huì)實(shí)現(xiàn)自定義的取消協(xié)議所灸,則轉(zhuǎn)換對(duì)象后取消。否則直接將這個(gè)object從字典中刪除炫七。
至此爬立,取消當(dāng)前圖片下載步驟完畢。

重點(diǎn)二

調(diào)用SDWebImageManager万哪,創(chuàng)建下載圖片的operation侠驯。

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

該方法的內(nèi)部實(shí)現(xiàn)是SDWebImage的核心,所有的精華都在這里奕巍。
實(shí)現(xiàn)中多次使用dispatch_main_sync_safedispatch_main_async_safe吟策。他們倆分別對(duì)應(yīng)兩個(gè)宏,一是為防止在主線程執(zhí)行主線程操作發(fā)生死鎖伍绳;二是避免不必要的開銷踊挠。dispatch_async不管怎么說都會(huì)有一定的開銷吧(此處存疑)。

#define dispatch_main_sync_safe(block)\
    if ([NSThread isMainThread]) {\
        block();\
    } else {\
        dispatch_sync(dispatch_get_main_queue(), block);\
    }

#define dispatch_main_async_safe(block)\
    if ([NSThread isMainThread]) {\
        block();\
    } else {\
        dispatch_async(dispatch_get_main_queue(), block);\
    }

SDWebImageManager方法downloadImageWithURL具體步驟

第一步

驗(yàn)證url冲杀,如果是字符串轉(zhuǎn)換為NSURL效床,如果不是NSURL類型,url置為nil权谁。

    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    // Prevents app crashing on argument type error like sending NSNull instead of NSURL
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }

第二步

創(chuàng)建一個(gè)SDWebImageCombinedOperation對(duì)象剩檀,代表一個(gè)圖片加載任務(wù),但是實(shí)際下載圖片的事是由另一個(gè)Operation來做旺芽,該類也實(shí)現(xiàn)了SDWebImageOperation協(xié)議沪猴。因?yàn)榭赡軙?huì)在block中調(diào)用operation,所以先處理處理好循環(huán)引用問題采章。

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

第三步

    BOOL isFailedUrl = NO;
    //該步驟可能會(huì)出現(xiàn)多線程讀取問題运嗜,所以添加@synchronized同步鎖。
    @synchronized (self.failedURLs) {
        isFailedUrl = [self.failedURLs containsObject:url];
    }

    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;
    }

第四步

將operation加進(jìn)數(shù)組中悯舟,需要添加同步鎖担租,保證數(shù)組的讀寫安全。

@synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }

第五步

獲取該網(wǎng)絡(luò)圖片緩存用的key抵怎。
NSString *key = [self cacheKeyForURL:url]; 展開這個(gè)方法是:

- (NSString *)cacheKeyForURL:(NSURL *)url {
    if (self.cacheKeyFilter) {
        return self.cacheKeyFilter(url);
    }
    else {
        return [url absoluteString];
    }
}

這里如果給manager設(shè)置過cacheKeyFilter奋救,則會(huì)按照自己的設(shè)置返回一個(gè)字符串作為key,否則會(huì)直接返回url 的絕對(duì)路徑absoluteString反惕。

第六步

operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {

這一步是為上面第二步創(chuàng)建的operation對(duì)象設(shè)置cacheOperation尝艘。

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
    if (!doneBlock) {
        return nil;
    }
    //key不存在,報(bào)錯(cuò)
    if (!key) {
        doneBlock(nil, SDImageCacheTypeNone);
        return nil;
    }

    // First check the in-memory cache...
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        doneBlock(image, SDImageCacheTypeMemory);
        return nil;
    }
    //緩存中沒有姿染,創(chuàng)建任務(wù)異步讀取磁盤
    NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            return;
        }

        @autoreleasepool {
            UIImage *diskImage = [self diskImageForKey:key];
            //在磁盤中找到圖片背亥,并在允許的情況下存緩存
            if (diskImage && self.shouldCacheImagesInMemory) {
                NSUInteger cost = SDCacheCostForImage(diskImage);
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }

            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, SDImageCacheTypeDisk);
            });
        }
    });

    return operation;
}

第七步

在cacheOperation的doneBlock中。如果圖片取到了緩存圖片,則直接將圖片等信息通過completedBlock返回隘梨。
從runningOperation中刪除步驟二中創(chuàng)建的該operation程癌。

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];
            }

如果返回的圖片為nil,或者需要下載轴猎,則通過SDWebImageDownloader的方法創(chuàng)建下載圖片的operation

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageDownloaderOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageDownloaderCompletedBlock)completedBlock;

下載過程待會(huì)詳細(xì)分析嵌莉,這里先分析下載完成后的操作。
情形一:回調(diào)返回的error捻脖,如果不為空锐峭,則返回錯(cuò)誤給completedBlock。如果url有問題可婶,則把url添加到failedURLs中沿癞。
情形二:如果成功,則
先從failedURLs中刪除url矛渴,里面不包含也沒關(guān)系椎扬。
如果url對(duì)應(yīng)的圖片是url不變,但是圖片會(huì)變的具温,則不緩存蚕涤。
如果圖片需要轉(zhuǎn)換,則將圖片轉(zhuǎn)換后保存到內(nèi)存和磁盤中铣猩,調(diào)用block返回圖片揖铜。如果不需要轉(zhuǎn)換,則直接保存和回調(diào)block达皿。

下載過程解析

在downloader中有一個(gè)URLCallbacks的可變字典天吓,每一個(gè)url作為key,對(duì)應(yīng)一個(gè)數(shù)組(數(shù)組中是字典對(duì)象峦椰,字典中保存下載operation的progressBlock和completeBlock)龄寞,然后判斷該url是否是首次下載,如果是汤功,則調(diào)用創(chuàng)建operation的block萄焦,否則直接返回沒有初始化的operation(nil)。

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return;
    }

    dispatch_barrier_sync(self.barrierQueue, ^{
        BOOL first = NO;
        if (!self.URLCallbacks[url]) {
            self.URLCallbacks[url] = [NSMutableArray new];
            first = YES;
        }

        // Handle single download of simultaneous download request for the same 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;

        if (first) {
            createCallback();
        }
    });
}

createCallBack內(nèi)部創(chuàng)建operation過程:
先創(chuàng)建一個(gè)NSMutableURLRequest冤竹,需要保證該url不被緩存。過程如下:

NSTimeInterval timeoutInterval = wself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        if (wself.headersFilter) {
            request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
        }
        else {
            request.allHTTPHeaderFields = wself.HTTPHeaders;
        }

然后創(chuàng)建一個(gè)operation(SDWebImageDownloaderOperation)對(duì)象茬射,將其放入downloadQueue中鹦蠕,并判斷是否設(shè)置過后進(jìn)先出的執(zhí)行順序,默認(rèn)是先進(jìn)先出的執(zhí)行在抛。

SDWebImageDownloaderOperation

SDWebImageDownloaderOperation為自定義的NSOperation钟病,其初始化方法為

- (id)initWithRequest:(NSURLRequest *)request
              options:(SDWebImageDownloaderOptions)options
             progress:(SDWebImageDownloaderProgressBlock)progressBlock
            completed:(SDWebImageDownloaderCompletedBlock)completedBlock
            cancelled:(SDWebImageNoParamsBlock)cancelBlock {
    if ((self = [super init])) {
        _request = request;
        _shouldDecompressImages = YES;
        _shouldUseCredentialStorage = YES;
        _options = options;
        _progressBlock = [progressBlock copy];
        _completedBlock = [completedBlock copy];
        _cancelBlock = [cancelBlock copy];
        _executing = NO;
        _finished = NO;
        _expectedSize = 0;
        responseFromCached = YES; // Initially wrong until `connection:willCacheResponse:` is called or not called
    }
    return self;
}

這里需要重寫start()方法,在start()方法中創(chuàng)建NSURLSession,并應(yīng)用起代理方法來更新progressBlock 和 completionHandler肠阱。還會(huì)在不同的結(jié)果時(shí)票唆,發(fā)送通知。

NSURLSession部分還沒理順屹徘,會(huì)在后期進(jìn)行更新

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末走趋,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子噪伊,更是在濱河造成了極大的恐慌簿煌,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件鉴吹,死亡現(xiàn)場(chǎng)離奇詭異姨伟,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)豆励,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門夺荒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人良蒸,你說我怎么就攤上這事技扼。” “怎么了诚啃?”我有些...
    開封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵淮摔,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我始赎,道長(zhǎng)和橙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任造垛,我火速辦了婚禮魔招,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘五辽。我一直安慰自己办斑,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開白布杆逗。 她就那樣靜靜地躺著乡翅,像睡著了一般。 火紅的嫁衣襯著肌膚如雪罪郊。 梳的紋絲不亂的頭發(fā)上蠕蚜,一...
    開封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音悔橄,去河邊找鬼靶累。 笑死腺毫,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的挣柬。 我是一名探鬼主播潮酒,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼邪蛔!你這毒婦竟也來了急黎?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤店溢,失蹤者是張志新(化名)和其女友劉穎叁熔,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體床牧,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡荣回,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了戈咳。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片心软。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖著蛙,靈堂內(nèi)的尸體忽然破棺而出删铃,到底是詐尸還是另有隱情,我是刑警寧澤踏堡,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布猎唁,位于F島的核電站,受9級(jí)特大地震影響顷蟆,放射性物質(zhì)發(fā)生泄漏诫隅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一帐偎、第九天 我趴在偏房一處隱蔽的房頂上張望逐纬。 院中可真熱鬧,春花似錦削樊、人聲如沸豁生。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽甸箱。三九已至,卻和暖如春迅脐,著一層夾襖步出監(jiān)牢的瞬間芍殖,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工仪际, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留围小,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓树碱,卻偏偏與公主長(zhǎng)得像肯适,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子成榜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

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