SDWebImage源碼之SDWebImageDownloader

顧名思義,SDWebImageDownloaderSDWebImage庫中負(fù)責(zé)圖片下載的管理工作凫岖。它維護(hù)著下載圖片用的NSURLSession江咳,但不直接與網(wǎng)絡(luò)打交道。它管理著一系列的SDWebImageDownloaderOperation哥放,下載的實(shí)際工作都由SDWebImageDownloaderOperation進(jìn)行歼指。

這兩個(gè)類合起來成為了一個(gè)異步并發(fā)機(jī)制的設(shè)計(jì)范本,可以作為后續(xù)開發(fā)類似機(jī)制的參考甥雕。


以下代碼和分析都基于041842bf085cbb711f0d9e099e6acbf6fd533b0c這個(gè)commit踩身。

SDWebImageDownloader & SDWebImageDownloaderOperation

@interface SDWebImageDownloader () <NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {

    // Identify the operation that runs this task and pass it the delegate method
    SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:dataTask];

    [dataOperation URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
}
@end

---

@interface SDWebImageDownloaderOperation : NSOperation <NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
// This is weak because it is injected by whoever manages this session. If this gets nil-ed out, we won't be able to run
// the task associated with this operation
@property (weak, nonatomic, nullable) NSURLSession *unownedSession;
// This is set if we're using not using an injected NSURLSession. We're responsible of invalidating this one
@property (strong, nonatomic, nullable) NSURLSession *ownedSession;

- (void)start {
        ...
        NSURLSession *session = self.unownedSession;
        if (!self.unownedSession) {
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            sessionConfig.timeoutIntervalForRequest = 15;
            
            /**
             *  Create the session for this task
             *  We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
             *  method calls and completion handler calls.
             */
            self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
                                                              delegate:self
                                                         delegateQueue:nil];
            session = self.ownedSession;
        }
        
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;
    }
    
    [self.dataTask resume];
    ...
}
@end

SDWebImageDownloaderOperation類支持從外部設(shè)置session(多個(gè)Opeations共享一個(gè)session)和自己維護(hù)一個(gè)session兩種模式。
為了行為統(tǒng)一社露,SDWebImageDownloader類實(shí)現(xiàn)了NSURLSessionTaskDelegateNSURLSessionTaskDelegate這兩個(gè)協(xié)議挟阻,但是把這些協(xié)議的具體實(shí)現(xiàn)都轉(zhuǎn)交給了SDWebImageDownloaderOperation

@interface SDWebImageDownloader () <NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
@property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, SDWebImageDownloaderOperation *> *URLOperations;

- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                                                   forURL:(nullable NSURL *)url
                                           createCallback:(SDWebImageDownloaderOperation *(^)())createCallback {
    ...
    dispatch_barrier_sync(self.barrierQueue, ^{
        SDWebImageDownloaderOperation *operation = self.URLOperations[url];
        if (!operation) {
            operation = createCallback();
            self.URLOperations[url] = operation;

            __weak SDWebImageDownloaderOperation *woperation = operation;
            operation.completionBlock = ^{
              SDWebImageDownloaderOperation *soperation = woperation;
              if (!soperation) return;
              if (self.URLOperations[url] == soperation) {
                  [self.URLOperations removeObjectForKey:url];
              };
            };
        }
    });
    ...
}

- (void)cancel:(nullable SDWebImageDownloadToken *)token {
    dispatch_barrier_async(self.barrierQueue, ^{
        SDWebImageDownloaderOperation *operation = self.URLOperations[token.url];
        BOOL canceled = [operation cancel:token.downloadOperationCancelToken];
        if (canceled) {
            [self.URLOperations removeObjectForKey:token.url];
        }
    });
}

@end

為了保證同一個(gè)URL只發(fā)出一個(gè)網(wǎng)絡(luò)請(qǐng)求峭弟,SDWebImageDownloader維護(hù)了一個(gè)以URL為key的字典self.URLOperations保存所有的SDWebImageDownloaderOperation附鸽。

這個(gè)方法可能會(huì)在任何線程被調(diào)用,為了防止出現(xiàn)多線程同時(shí)操作self.URLOperations的問題瞒瘸,SDWebImageDownloader維護(hù)了一個(gè)專門的隊(duì)列處理對(duì)self.URLOperations的操作坷备。
仔細(xì)看這里的代碼會(huì)發(fā)現(xiàn)一個(gè)問題,NSOperation的completionBlock也對(duì)self.URLOperations進(jìn)行了操作情臭,但是它沒有被放在self.barrierQueue里執(zhí)行省撑。已經(jīng)有網(wǎng)友針對(duì)這個(gè)問題給SDWebImage提了Pull Request赌蔑。

@interface SDWebImageDownloader () <NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
@property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, SDWebImageDownloaderOperation *> *URLOperations;

- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                                                   forURL:(nullable NSURL *)url
                                           createCallback:(SDWebImageDownloaderOperation *(^)())createCallback {
    ...
  __block SDWebImageDownloadToken *token = nil;
  id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
        token = [SDWebImageDownloadToken new];
        token.url = url;
        token.downloadOperationCancelToken = downloadOperationCancelToken;
  return token;
}
@end

---

typedef NSMutableDictionary<NSString *, id> SDCallbacksDictionary;

@interface SDWebImageDownloaderOperation ()
@property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *callbackBlocks;

- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
    if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
    if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
    dispatch_barrier_async(self.barrierQueue, ^{
        [self.callbackBlocks addObject:callbacks];
    });
    return callbacks;
}

- (BOOL)cancel:(nullable id)token {
    __block BOOL shouldCancel = NO;
    dispatch_barrier_sync(self.barrierQueue, ^{
        [self.callbackBlocks removeObjectIdenticalTo:token];
        if (self.callbackBlocks.count == 0) {
            shouldCancel = YES;
        }
    });
    if (shouldCancel) {
        [self cancel];
    }
    return shouldCancel;
}

@end

上面提到,多個(gè)地方可能同時(shí)觸發(fā)同一個(gè)URL的圖片下載竟秫,'SDWebImageDownloader'重用了下載用的SDWebImageDownloaderOperation對(duì)象惯雳。但是這一優(yōu)化對(duì)于外部是透明的,SDWebImageDownloaderOperation要保證每一個(gè)調(diào)用下載的對(duì)象都能正確接收到下載完成的回調(diào)鸿摇。
SDWebImageDownloaderOperation保存了callbackBlocks這個(gè)數(shù)組用于維護(hù)所有的回調(diào)block。下載進(jìn)度更新和下載完成的回調(diào)block被合起來以SDCallbacksDictionary的形式保存在callbackBlocks里劈猿。同時(shí)拙吉,這個(gè)SDCallbacksDictionary的指針還被返回作為取消請(qǐng)求用的標(biāo)記。
SDWebImageDownloader一樣揪荣,SDWebImageDownloaderOperation也維護(hù)了一個(gè)self.barrierQueue用作操作self.callbackBlocks筷黔。

#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
    if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
        block();\
    } else {\
        dispatch_async(dispatch_get_main_queue(), block);\
    }
#endif

- (void)callCompletionBlocksWithImage:(nullable UIImage *)image
                            imageData:(nullable NSData *)imageData
                                error:(nullable NSError *)error
                             finished:(BOOL)finished {
    NSArray<id> *completionBlocks = [self callbacksForKey:kCompletedCallbackKey];
    dispatch_main_async_safe(^{
        for (SDWebImageDownloaderCompletedBlock completedBlock in completionBlocks) {
            completedBlock(image, imageData, error, finished);
        }
    });
}

不管圖片下載完成還是出現(xiàn)異常,completionBlock都會(huì)被拋到主線程處理仗颈。

SDWebImageDownloaderTests

/**
 *  Category for SDWebImageDownloader so we can access the operationClass
 */
@interface SDWebImageDownloader ()
@property (assign, nonatomic, nullable) Class operationClass;
@property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue;

- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                                                   forURL:(nullable NSURL *)url
                                           createCallback:(SDWebImageDownloaderOperation *(^)())createCallback;
@end

可以通過給要測(cè)試的類加一個(gè)Category的方式來暴露其私有屬性和方法給測(cè)試佛舱,不需要為了測(cè)試把私有方法提到接口中。


SDWebImage源碼系列

SDWebImage源碼之SDImageCache
SDWebImage源碼之SDWebImageDownloader
SDWebImage源碼之SDWebImageManager

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末挨决,一起剝皮案震驚了整個(gè)濱河市请祖,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌脖祈,老刑警劉巖肆捕,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異盖高,居然都是意外死亡慎陵,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門喻奥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來席纽,“玉大人,你說我怎么就攤上這事撞蚕∪筇荩” “怎么了?”我有些...
    開封第一講書人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵甥厦,是天一觀的道長仆救。 經(jīng)常有香客問我,道長矫渔,這世上最難降的妖魔是什么彤蔽? 我笑而不...
    開封第一講書人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮庙洼,結(jié)果婚禮上顿痪,老公的妹妹穿的比我還像新娘镊辕。我一直安慰自己,他們只是感情好蚁袭,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開白布征懈。 她就那樣靜靜地躺著,像睡著了一般揩悄。 火紅的嫁衣襯著肌膚如雪卖哎。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,182評(píng)論 1 299
  • 那天删性,我揣著相機(jī)與錄音亏娜,去河邊找鬼。 笑死蹬挺,一個(gè)胖子當(dāng)著我的面吹牛维贺,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播巴帮,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼溯泣,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了榕茧?” 一聲冷哼從身側(cè)響起垃沦,我...
    開封第一講書人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎用押,沒想到半個(gè)月后栏尚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡译仗,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了咱圆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖匈睁,靈堂內(nèi)的尸體忽然破棺而出航唆,到底是詐尸還是另有隱情糯钙,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布演闭,位于F島的核電站米碰,受9級(jí)特大地震影響吕座,放射性物質(zhì)發(fā)生泄漏吴趴。R本人自食惡果不足惜锣枝,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一畦贸、第九天 我趴在偏房一處隱蔽的房頂上張望薄坏。 院中可真熱鬧胶坠,春花似錦沈善、人聲如沸帖蔓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽写妥。三九已至珍特,卻和暖如春扎筒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背辞色。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留棕所,地道東北人琳省。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓拢蛋,卻偏偏與公主長得像谆棱,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子坪郭,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353

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