前言
我們在第一篇文章《SDWebImage源碼解析<一>》已經(jīng)了解到SDWebImage是通過 SDWebImageManager
類進(jìn)行協(xié)調(diào),調(diào)用 SDImageCache
與 SDWebImageDownloader
來實現(xiàn)圖片的緩存查詢與網(wǎng)絡(luò)下載的。今天我們就來分析一下SDImageCache
和SDWebImageDownloader
。
SDImageCache
該類維護(hù)了一個內(nèi)存緩存與一個可選的磁盤緩存。同時菊卷,磁盤緩存的寫操作是異步的煤率,所以它不會對 UI 造成不必要的影響嗜闻。
存儲圖片
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
if (!image || !key) {
return;
}
// 內(nèi)存緩存 前提是設(shè)置了需要進(jìn)行秘车,將其存入 NSCache 中典勇,同時傳入圖片的消耗值,cost 為像素值(當(dāng)內(nèi)存受限或者所有緩存對象的總代價超過了最大允許的值時叮趴,緩存會移除其中的一些對象)
if (self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
// 磁盤緩存
if (toDisk) {
// 將緩存操作作為一個任務(wù)放入ioQueue中異步執(zhí)行
dispatch_async(self.ioQueue, ^{
NSData *data = imageData;
if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
// 需要確定圖片是PNG還是JPEG割笙。PNG圖片容易檢測,因為有一個唯一簽名眯亦。PNG圖像的前8個字節(jié)總是包含以下值:137 80 78 71 13 10 26 10
// 在imageData為nil的情況下假定圖像為PNG伤溉。我們將其當(dāng)作PNG以避免丟失透明度。
int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
BOOL imageIsPng = hasAlpha;
// 而當(dāng)有圖片數(shù)據(jù)時搔驼,我們檢測其前綴谈火,確定圖片的類型
if ([imageData length] >= [kPNGSignatureData length]) {
imageIsPng = ImageDataHasPNGPreffix(imageData);
}
// 如果 image 是 PNG 格式,就是用 UIImagePNGRepresentation 將其轉(zhuǎn)化為 NSData舌涨,否則按照 JPEG 格式轉(zhuǎn)化糯耍,并且壓縮質(zhì)量為 1,即無壓縮
if (imageIsPng) {
data = UIImagePNGRepresentation(image);
}
else {
data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
}
#else
data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
}
// 創(chuàng)建緩存文件并存儲圖片(使用 fileManager)
if (data) {
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// 根據(jù)image的key獲取緩存路徑
NSString *cachePathForKey = [self defaultCachePathForKey:key];
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
[_fileManager createFileAtPath:cachePathForKey contents:data attributes:nil];
// 不適用iCloud備份
if (self.shouldDisableiCloud) {
[fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
});
}
}
查詢圖片
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
// 對doneBlock囊嘉、key判空 查找內(nèi)存緩存
if (!doneBlock) {
return nil;
}
if (!key) {
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}
// 首先檢查內(nèi)存緩存(查詢是同步的)温技,如果查找到,則直接回調(diào) doneBlock 并返回
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) { // isCancelled初始默認(rèn)值為NO
return;
}
@autoreleasepool {
// 檢查磁盤緩存(查詢是異步的)扭粱,如果查找到舵鳞,則將其放到內(nèi)存緩存,并調(diào)用 doneBlock 回調(diào)
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
// 緩存至內(nèi)存(NSCache)中
[self.memCache setObject:diskImage forKey:key cost:cost];
}
// 返回主線程設(shè)置圖片
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, SDImageCacheTypeDisk);
});
}
});
return operation;
}
通過代碼可以看到operation雖然沒有具體的內(nèi)容琢蛤,但是我們可以在外部調(diào)用operation的cancel方法來改變isCancelled的值蜓堕。這樣做對從內(nèi)存緩存中查找到圖片的本次操作查詢過程沒有影響,但是如果本次查詢過程是在磁盤緩存中進(jìn)行的博其,就會受到影響套才,autoreleasepool{}代碼塊不再執(zhí)行。而在這段代碼塊完成了這樣的工作:將磁盤緩存取出進(jìn)行內(nèi)存緩存慕淡,在線程執(zhí)行完成回調(diào)背伴。因此可以看到這個返回的NSOpeation值可以幫助我們在外部控制不再進(jìn)行磁盤緩存查詢和內(nèi)存緩存?zhèn)浞莸牟僮鳎瑲w根結(jié)底就是向外部暴漏了取消操作的接口峰髓。
清除圖片
對于清理方法cleanDiskWithCompletionBlock:
傻寂,有兩個條件:文件的緩存有效期及最大緩存空間大小。
- 文件的緩存有效期可以通過maxCacheAge屬性來設(shè)置携兵,默認(rèn)是1周的時間疾掰。如果文件的緩存時間超過這個時間值,則將其移除徐紧。
- 最大緩存空間大小是通過maxCacheSize屬性來設(shè)置的个绍,如果所有緩存文件的總大小超過這一大小勒葱,則會按照文件最后修改時間的逆序,以每次一半的遞歸來移除那些過早的文件巴柿,直到緩存的實際大小小于我們設(shè)置的最大使用空間凛虽。
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
// 枚舉器預(yù)先獲取緩存文件的有用的屬性
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
NSUInteger currentCacheSize = 0;
// 枚舉緩存文件夾中所有文件,該迭代有兩個目的:移除比過期日期更老的文件广恢;存儲文件屬性以備后面執(zhí)行基于緩存大小的清理操作
NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
// 跳過文件夾
if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// 移除早于有效期的老文件
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
// 存儲文件的引用并計算所有文件的總大小
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
[cacheFiles setObject:resourceValues forKey:fileURL];
}
for (NSURL *fileURL in urlsToDelete) {
[_fileManager removeItemAtURL:fileURL error:nil];
}
// 如果磁盤緩存的大小超過我們配置的最大大小凯旋,則執(zhí)行基于文件大小的清理,我們首先刪除最老的文件
if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
// 以設(shè)置的最大緩存大小的一半值作為清理目標(biāo)
const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
// 按照最后修改時間來排序剩下的緩存文件
NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
// 刪除文件钉迷,直到緩存總大小降到我們期望的大小
for (NSURL *fileURL in sortedFiles) {
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();
});
}
});
}
SDWebImageDownloaderOptions
在下載的過程中至非,程序會根據(jù)設(shè)置的不同的下載選項,而執(zhí)行不同的操作糠聪。下載選項由枚舉 SDWebImageDownloaderOptions
定義荒椭,具體如下:
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
SDWebImageDownloaderLowPriority = 1 << 0,
/// 漸進(jìn)式下載,如果設(shè)置了這個選項舰蟆,會在下載過程中趣惠,每次接收到一段chunk數(shù)據(jù)就調(diào)用一次完成回調(diào)(注意是完成回調(diào))回調(diào)中的image參數(shù)為未下載完成的部分圖像
SDWebImageDownloaderProgressiveDownload = 1 << 1,
/// 通常情況下request阻止使用NSURLCache. 這個選項會用默認(rèn)策略使用NSURLCache
SDWebImageDownloaderUseNSURLCache = 1 << 2,
/// 如果從NSURLCache中讀取圖片,會在調(diào)用完成block時身害,傳遞空的image或imageData \
* (to be combined with `SDWebImageDownloaderUseNSURLCache`).
SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
/// 系統(tǒng)為iOS 4+時味悄,如果應(yīng)用進(jìn)入后臺,繼續(xù)下載塌鸯。這個選項是為了實現(xiàn)在后臺申請額外的時間來完成請求侍瑟。如果后臺任務(wù)到期,操作會被取消丙猬。
SDWebImageDownloaderContinueInBackground = 1 << 4,
/// 通過設(shè)置NSMutableURLRequest.HTTPShouldHandleCookies = YES的方式來處理存儲在NSHTTPCookieStore的cookies
SDWebImageDownloaderHandleCookies = 1 << 5,
/// 允許不受信任的SSL證書涨颜,在測試環(huán)境中很有用,在生產(chǎn)環(huán)境中要謹(jǐn)慎使用
SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
/// 將圖片下載放到高優(yōu)先級隊列中
SDWebImageDownloaderHighPriority = 1 << 7,
};
下面我們看一下SDWebImageDownloaderOperation
對NSOperation的-start
方法的重寫茧球,畢竟這是完成下載任務(wù)的核心代碼
- (void)start {
@synchronized (self) {
if (self.isCancelled) {
self.finished = YES;
// 將各個屬性置空庭瑰。包括取消回調(diào)、完成回調(diào)袜腥、進(jìn)度回調(diào)见擦,用于網(wǎng)絡(luò)連接的connection钉汗,用于拼接數(shù)據(jù)的imageData羹令、記錄當(dāng)前線程的屬性thread。
[self reset];
return;
}
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
// 使用UIApplication的beginBackgroundTaskWithExpirationHandler方法向系統(tǒng)借用一點時間损痰,繼續(xù)執(zhí)行下面的代碼來完成connection的創(chuàng)建和進(jìn)行下載任務(wù)福侈。
// 在后臺任務(wù)執(zhí)行時間超過最大時間時,也就是后臺任務(wù)過期執(zhí)行過期回調(diào)卢未。在回調(diào)主動將這個后臺任務(wù)結(jié)束肪凛。
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
__weak __typeof__ (self) wself = self;
UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
__strong __typeof (wself) sself = wself;
if (sself) {
[sself cancel];
[app endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
#endif
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;// 標(biāo)記狀態(tài)
self.thread = [NSThread currentThread]; // 記錄當(dāng)前線程
}
[self.dataTask resume];
if (self.dataTask) {
if (self.progressBlock) {
self.progressBlock(0, NSURLResponseUnknownLength);
}
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
}
else {
if (self.completedBlock) {
self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
}
}
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
[app endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
#endif
}
SDWebImageDownloader
SDWebImageDownloader有一個重要的屬性executionOrder
代表著下載操作執(zhí)行的順序堰汉,它是一個SDWebImageDownloaderExecutionOrder
枚舉類型
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
// 默認(rèn)值,所有的下載操作以隊列類型 (先進(jìn)先出)執(zhí)行.
SDWebImageDownloaderFIFOExecutionOrder,
// 所有的下載操作以棧類型 (后進(jìn)先出)執(zhí)行.
SDWebImageDownloaderLIFOExecutionOrder
};
默認(rèn)是SDWebImageDownloaderFIFOExecutionOrder
伟墙,是在init方法中設(shè)置的翘鸭。如果設(shè)置了后進(jìn)先出,在下載操作添加到下載隊列中時戳葵,會依據(jù)這個值添加依賴關(guān)系就乓,使得最后添加操作出在依賴關(guān)系鏈條中的第一項,因而會優(yōu)先下載最后添加的操作任務(wù)拱烁。
SDWebImageDownloader
還提供了其他幾個重要的對外接口(包括屬性和方法):
1.BOOL shouldDecompressImages
是否需要解壓生蚁,在init中設(shè)置默認(rèn)值為YES,在下載操作創(chuàng)建之后將值傳遞給操作的同名屬性戏自。
解壓下載或緩存的圖片可以提升性能邦投,但是會消耗很多內(nèi)存
默認(rèn)是YES,如果你會遇到因為過高的內(nèi)存消耗引起的崩潰將它設(shè)置為NO擅笔。
2.NSInteger maxConcurrentDownloads
放到下載隊列中的下載操作的總數(shù)志衣,是一個瞬間值,因為下載操作一旦執(zhí)行完成剂娄,就會從隊列中移除蠢涝。
3.NSUInteger currentDownloadCount
下載操作的超時時長默認(rèn)是15.0,即request的超時時長阅懦,若設(shè)置為0和二,在創(chuàng)建request的時候依然使用15.0。
只讀耳胎。
4.NSURLCredential *urlCredential
為request操作設(shè)置默認(rèn)的URL憑據(jù)惯吕,具體實施為:在將操作添加到隊列之前,將操作的credential屬性值設(shè)置為urlCredential
5.NSString *username和NSString *passwords
如果設(shè)置了用戶名和密碼:在將操作添加到隊列之前怕午,會將操作的credential屬性值設(shè)置為[NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession]
废登,而忽略了屬性值urlCredential。
6.- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;
為HTTP header設(shè)置value郁惜,用來追加到每個下載對應(yīng)的HTTP request, 若傳遞的value為nil堡距,則將對應(yīng)的field移除。
擴(kuò)展里面定義了一個HTTPHeaders屬性(NSMutableDictionary類型)用來存儲所有設(shè)置好的header和對應(yīng)value兆蕉。
在創(chuàng)建request之后緊接著會將HTTPHeaders賦給request羽戒,request.allHTTPHeaderFields = self.HTTPHeaders
;
7.- (NSString *)valueForHTTPHeaderField:(NSString *)field;
返回指定的HTTP header field對應(yīng)的value
8.SDWebImageDownloaderHeadersFilterBlock headersFilter
設(shè)置一個過濾器,為下載圖片的HTTP request選取header.意味著最終使用的headers是經(jīng)過這個block過濾之后的返回值虎韵。
9.- (void)setOperationClass:(Class)operationClass;
設(shè)置一個SDWebImageDownloaderOperation
的子類 易稠,在每次 SDWebImage 構(gòu)建一個下載圖片的請求操作的時候作為默認(rèn)的NSOperation使用.
參數(shù)operationClass為要設(shè)置的默認(rèn)下載操作的SDWebImageDownloaderOperation
的子類。 傳遞 nil 會恢復(fù)為SDWebImageDownloaderOperation
包蓝。
以下兩個方法是下載控制方法了
- (id <SDWebImageOperation>)downloadImageWithURL: options: progress: completed:
這個方法用指定的URL創(chuàng)建一個異步下載實例驶社。
有關(guān)completedBlock
回調(diào)的一些解釋:下載完成的時候block會調(diào)用一次.
沒有使用SDWebImageDownloaderProgressiveDownload
選項的情況下企量,如果下載成功會設(shè)置image參數(shù),如果出錯,會根據(jù)錯誤設(shè)置error參數(shù). 最后一個參數(shù)總是YES. 如果使用了SDWebImageDownloaderProgressiveDownload
選項亡电,這個block會使用部分image的對象有間隔地重復(fù)調(diào)用届巩,同時finished參數(shù)設(shè)置為NO,直到使用完整的image對象和值為YES的finished參數(shù)進(jìn)行最后一次調(diào)用.如果出錯,finished參數(shù)總是YES.
- (void)setSuspended:(BOOL)suspended;
設(shè)置下載隊列的掛起(暫停)狀態(tài)份乒。若為YES姆泻,隊列不再開啟新的下載操作,再向隊列里面添加的操作也不會被開啟冒嫡,但是正在執(zhí)行的操作依然繼續(xù)執(zhí)行拇勃。
下面我們就來看一下下載方法的實現(xiàn)細(xì)節(jié):
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
__block SDWebImageDownloaderOperation *operation;
__weak __typeof(self)wself = self;
[self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// 創(chuàng)建請求對象,并根據(jù) options 參數(shù)設(shè)置其屬性
// 為了避免潛在的重復(fù)緩存(NSURLCache + SDImageCache)孝凌,如果沒有明確告知需要緩存方咆,則禁用圖片請求的緩存操作
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)建 SDWebImageDownloaderOperation 操作對象,傳入進(jìn)度回調(diào)蟀架、完成回調(diào)瓣赂、取消回調(diào)
// 配置信息包括是否需要認(rèn)證、優(yōu)先級
operation = [[wself.operationClass alloc] initWithRequest:request
inSession:self.session
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
// 從管理器的 callbacksForURL 中找出該 URL 所有的進(jìn)度處理回調(diào)并調(diào)用
// 將刪除所有回調(diào)的block放到隊列barrierQueue中使用barrier_sync方式執(zhí)行片拍,確保了在進(jìn)行調(diào)用完成回調(diào)之前所有的使用url對應(yīng)的回調(diào)的地方都是正確的數(shù)據(jù)煌集。
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
});
for (NSDictionary *callbacks in callbacksForURL) {
dispatch_async(dispatch_get_main_queue(), ^{
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
if (callback) callback(receivedSize, expectedSize);
});
}
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
// 從管理器的 callbacksForURL 中找出該 URL 所有的完成處理回調(diào)并調(diào)用
// 如果 finished 為 YES,則將該 url 對應(yīng)的回調(diào)信息從 URLCallbacks 中刪除
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_barrier_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
if (finished) {
[sself.URLCallbacks removeObjectForKey:url];
}
});
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
if (callback) callback(image, data, error, finished);
}
}
cancelled:^{
// 取消操作將該 url 對應(yīng)的回調(diào)信息從 URLCallbacks 中刪除
SDWebImageDownloader *sself = wself;
if (!sself) return;
dispatch_barrier_async(sself.barrierQueue, ^{
[sself.URLCallbacks removeObjectForKey:url];
});
}];
// 設(shè)置是否需要解壓
operation.shouldDecompressImages = wself.shouldDecompressImages;
// 設(shè)置進(jìn)行網(wǎng)絡(luò)訪問驗證的憑據(jù)
if (wself.urlCredential) {
operation.credential = wself.urlCredential;
} else if (wself.username && wself.password) {
operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
}
// 根據(jù)下載選項SDWebImageDownloaderHighPriority設(shè)置優(yōu)先級
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
// 將操作加入到操作隊列 downloadQueue 中
// 如果是 LIFO 順序捌省,則將新的操作作為原隊列中最后一個操作的依賴苫纤,然后將新操作設(shè)置為最后一個操作
[wself.downloadQueue addOperation:operation];
// 根據(jù)executionOrder設(shè)置操作的依賴關(guān)系
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
}];
return operation;
}
重點就是addProgressCallback: completedBlock: forURL: createCallback:的執(zhí)行了,SDWebImageDownloader
將外部傳來的進(jìn)度回調(diào)纲缓、完成回調(diào)卷拘、url直接傳遞給這個方法,并實現(xiàn)創(chuàng)建下載操作的代碼塊作為這個方法的createCallback參數(shù)值祝高。下面就看一下這個方法的實現(xiàn)細(xì)節(jié):
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
// 對URL判空栗弟,如果為空,直接執(zhí)行完成回調(diào)工闺。
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return;
}
/*
對dispatch_barrier_sync函數(shù)的解釋:
向分配隊列提交一個同步執(zhí)行的barrier block乍赫。與dispatch_barrier_async不同,這個函數(shù)直到barrier block執(zhí)行完畢才會返回陆蟆,在當(dāng)前隊列調(diào)用這個函數(shù)會導(dǎo)致死鎖雷厂。當(dāng)barrier block被放進(jìn)一個私有的并行隊列后,它不會被立刻執(zhí)行遍搞。實際為罗侯,隊列會等待直到當(dāng)前正在執(zhí)行的blocks執(zhí)行完畢器腋。到那個時刻溪猿,隊列才會自己執(zhí)行barrier block钩杰。而任何放到 barrier block之后的block直到 barrier block執(zhí)行完畢才會執(zhí)行。
傳遞的隊列參數(shù)應(yīng)該是你自己用dispatch_queue_create函數(shù)創(chuàng)建的一個并行隊列诊县。如果你傳遞一個串行隊列或者全局并行隊列讲弄,這個函數(shù)的行為和 dispatch_sync相同。
與dispatch_barrier_async不同依痊,它不會對目標(biāo)隊列進(jìn)行強(qiáng)引用(retain操作)避除。因為調(diào)用這個方法是同步的,它“借用”了調(diào)用者的引用胸嘁。而且瓶摆,沒有對block進(jìn)行Block_copy操作。
作為對其優(yōu)化性宏,這個函數(shù)會在可能的情況下在當(dāng)前線程喚起barrier block群井。
*/
// 為確保不會死鎖,當(dāng)前隊列是另一個隊列毫胜,而不能是self.barrierQueue书斜。
dispatch_barrier_sync(self.barrierQueue, ^{
BOOL first = NO;
if (!self.URLCallbacks[url]) {
self.URLCallbacks[url] = [NSMutableArray new];
first = YES;
}
/*
URLCallbacks字典類型key為NSURL類型,value為NSMutableArray類型酵使,value只包含著一個元素荐吉,這個元素是一個NSMutableDictionary類型,它的key為NSString代表著回調(diào)類型口渔,value為block样屠,是對應(yīng)的回調(diào)
*/
// 同一時刻對相同url的多個下載請求只進(jìn)行一次下載
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();
/* 解釋
若url第一次綁定它的回調(diào),也就是第一次使用這個url創(chuàng)建下載任務(wù)缺脉,則執(zhí)行一次創(chuàng)建回調(diào)瞧哟。
在創(chuàng)建回調(diào)中創(chuàng)建下載操作,dispatch_barrier_sync執(zhí)行確保同一時間只有一個線程操作URLCallbacks屬性枪向,也就是確保了下面創(chuàng)建過程中在給operation傳遞回調(diào)的時候能取到正確的self.URLCallbacks[url]值勤揩。同時保證后面有相同的url再次創(chuàng)建時,if (!self.URLCallbacks[url])分支不再進(jìn)入秘蛔,first==NO,也就不再繼續(xù)調(diào)用創(chuàng)建回調(diào)陨亡。這樣就確保了同一個url對應(yīng)的圖片不會被重復(fù)下載。
而下載器的完成回調(diào)中會將url從self.URLCallbacks中remove深员,雖然remove掉了负蠕,但是再次使用這個url進(jìn)行下載圖片的時候,Manager會向緩存中讀取下載成功的圖片了,而不是無腦地直接添加下載任務(wù)倦畅;即使之前的下載是失敗的(也就是說沒有緩存)遮糖,這樣繼續(xù)添加下載任務(wù)也是合情合理的。
// 因此準(zhǔn)確地說叠赐,將這個block放到并行隊列dispatch_barrier_sync執(zhí)行確保了欲账,同一個url的圖片不會同一時刻進(jìn)行多次下載.
// 這樣做還使得下載操作的創(chuàng)建同步進(jìn)行屡江,因為一個新的下載操作還沒有創(chuàng)建完成,self.barrierQueue會繼續(xù)等待它完成赛不,然后才能執(zhí)行下一個添加下載任務(wù)的block惩嘉。所以說SD添加下載任務(wù)是同步的,而且都是在self.barrierQueue這個并行隊列中踢故,同步添加任務(wù)文黎。這樣也保證了根據(jù)executionOrder設(shè)置依賴關(guān)是正確的。換句話說如果創(chuàng)建下載任務(wù)不是使用dispatch_barrier_sync完成的殿较,而是使用異步方法 耸峭,雖然依次添加創(chuàng)建下載操作A、B淋纲、C的任務(wù)抓艳,但實際創(chuàng)建順序可能為A、C帚戳、B玷或,這樣當(dāng)executionOrder的值是SDWebImageDownloaderLIFOExecutionOrder,設(shè)置的操作依賴關(guān)系就變成了A依賴C片任,C依賴B
// 但是添加之后的下載依然是在下載隊列downloadQueue中異步執(zhí)行偏友,絲毫不會影響到下載效率。
// 以上就是說了SD下載的關(guān)鍵點:創(chuàng)建下載任務(wù)在barrierQueue隊列中对供,執(zhí)行下載在downloadQueue隊列中位他。
*/
}
});
}
關(guān)于SDWebImage的源碼閱讀就到這里結(jié)束,有什么不對的地方产场,歡迎指正鹅髓。
相關(guān)資料:
通讀SDWebImage①--總體梳理、下載和緩存
SDWebImage 源碼閱讀筆記(三)