前言
我們在第一篇文章《SDWebImage源碼解析<一>》已經(jīng)了解到SDWebImage是通過 SDWebImageManager 類進(jìn)行協(xié)調(diào),調(diào)用 SDImageCache與 SDWebImageDownloader 來實(shí)現(xiàn)圖片的緩存查詢與網(wǎng)絡(luò)下載的。今天我們就來分析一下SDImageCache和SDWebImageDownloader。
SDImageCache
該類維護(hù)了一個(gè)內(nèi)存緩存與一個(gè)可選的磁盤緩存。同時(shí),磁盤緩存的寫操作是異步的,所以它不會對 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 中,同時(shí)傳入圖片的消耗值,cost 為像素值(當(dāng)內(nèi)存受限或者所有緩存對象的總代價(jià)超過了最大允許的值時(shí),緩存會移除其中的一些對象)
if (self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
// 磁盤緩存
if (toDisk) {
// 將緩存操作作為一個(gè)任務(wù)放入ioQueue中異步執(zhí)行
dispatch_async(self.ioQueue, ^{
NSData *data = imageData;
if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
// 需要確定圖片是PNG還是JPEG。PNG圖片容易檢測,因?yàn)橛幸粋€(gè)唯一簽名。PNG圖像的前8個(gè)字節(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ù)時(shí),我們檢測其前綴,確定圖片的類型
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)。因此可以看到這個(gè)返回的NSOpeation值可以幫助我們在外部控制不再進(jìn)行磁盤緩存查詢和內(nèi)存緩存?zhèn)浞莸牟僮鳎瑲w根結(jié)底就是向外部暴漏了取消操作的接口。
清除圖片
對于清理方法cleanDiskWithCompletionBlock:,有兩個(gè)條件:文件的緩存有效期及最大緩存空間大小。
- 文件的緩存有效期可以通過maxCacheAge屬性來設(shè)置,默認(rèn)是1周的時(shí)間。如果文件的緩存時(shí)間超過這個(gè)時(shí)間值,則將其移除。
- 最大緩存空間大小是通過maxCacheSize屬性來設(shè)置的,如果所有緩存文件的總大小超過這一大小,則會按照文件最后修改時(shí)間的逆序,以每次一半的遞歸來移除那些過早的文件,直到緩存的實(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;
// 枚舉緩存文件夾中所有文件,該迭代有兩個(gè)目的:移除比過期日期更老的文件;存儲文件屬性以備后面執(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;
}
// 存儲文件的引用并計(jì)算所有文件的總大小
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;
// 按照最后修改時(shí)間來排序剩下的緩存文件
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è)置的不同的下載選項(xiàng),而執(zhí)行不同的操作。下載選項(xiàng)由枚舉 SDWebImageDownloaderOptions定義,具體如下:
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
SDWebImageDownloaderLowPriority = 1 << 0,
/// 漸進(jìn)式下載,如果設(shè)置了這個(gè)選項(xiàng),會在下載過程中,每次接收到一段chunk數(shù)據(jù)就調(diào)用一次完成回調(diào)(注意是完成回調(diào))回調(diào)中的image參數(shù)為未下載完成的部分圖像
SDWebImageDownloaderProgressiveDownload = 1 << 1,
/// 通常情況下request阻止使用NSURLCache. 這個(gè)選項(xiàng)會用默認(rèn)策略使用NSURLCache
SDWebImageDownloaderUseNSURLCache = 1 << 2,
/// 如果從NSURLCache中讀取圖片,會在調(diào)用完成block時(shí),傳遞空的image或imageData \
* (to be combined with `SDWebImageDownloaderUseNSURLCache`).
SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
/// 系統(tǒng)為iOS 4+時(shí),如果應(yīng)用進(jìn)入后臺,繼續(xù)下載。這個(gè)選項(xiàng)是為了實(shí)現(xiàn)在后臺申請額外的時(shí)間來完成請求。如果后臺任務(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)先級隊(duì)列中
SDWebImageDownloaderHighPriority = 1 << 7,
};
下面我們看一下SDWebImageDownloaderOperation對NSOperation的-start方法的重寫,畢竟這是完成下載任務(wù)的核心代碼
- (void)start {
@synchronized (self) {
if (self.isCancelled) {
self.finished = YES;
// 將各個(gè)屬性置空。包括取消回調(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)借用一點(diǎn)時(shí)間,繼續(xù)執(zhí)行下面的代碼來完成connection的創(chuàng)建和進(jìn)行下載任務(wù)。
// 在后臺任務(wù)執(zhí)行時(shí)間超過最大時(shí)間時(shí),也就是后臺任務(wù)過期執(zhí)行過期回調(diào)。在回調(diào)主動將這個(gè)后臺任務(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有一個(gè)重要的屬性executionOrder代表著下載操作執(zhí)行的順序,它是一個(gè)SDWebImageDownloaderExecutionOrder枚舉類型
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
// 默認(rèn)值,所有的下載操作以隊(duì)列類型 (先進(jìn)先出)執(zhí)行.
SDWebImageDownloaderFIFOExecutionOrder,
// 所有的下載操作以棧類型 (后進(jìn)先出)執(zhí)行.
SDWebImageDownloaderLIFOExecutionOrder
};
默認(rèn)是SDWebImageDownloaderFIFOExecutionOrder,是在init方法中設(shè)置的。如果設(shè)置了后進(jìn)先出,在下載操作添加到下載隊(duì)列中時(shí),會依據(jù)這個(gè)值添加依賴關(guān)系,使得最后添加操作出在依賴關(guān)系鏈條中的第一項(xiàng),因而會優(yōu)先下載最后添加的操作任務(wù)。
SDWebImageDownloader還提供了其他幾個(gè)重要的對外接口(包括屬性和方法):
1.BOOL shouldDecompressImages
是否需要解壓,在init中設(shè)置默認(rèn)值為YES,在下載操作創(chuàng)建之后將值傳遞給操作的同名屬性。
解壓下載或緩存的圖片可以提升性能,但是會消耗很多內(nèi)存
默認(rèn)是YES,如果你會遇到因?yàn)檫^高的內(nèi)存消耗引起的崩潰將它設(shè)置為NO。
2.NSInteger maxConcurrentDownloads
放到下載隊(duì)列中的下載操作的總數(shù),是一個(gè)瞬間值,因?yàn)橄螺d操作一旦執(zhí)行完成,就會從隊(duì)列中移除。
3.NSUInteger currentDownloadCount
下載操作的超時(shí)時(shí)長默認(rèn)是15.0,即request的超時(shí)時(shí)長,若設(shè)置為0,在創(chuàng)建request的時(shí)候依然使用15.0。
只讀。
4.NSURLCredential *urlCredential
為request操作設(shè)置默認(rèn)的URL憑據(jù),具體實(shí)施為:在將操作添加到隊(duì)列之前,將操作的credential屬性值設(shè)置為urlCredential
5.NSString *username和NSString *passwords
如果設(shè)置了用戶名和密碼:在將操作添加到隊(duì)列之前,會將操作的credential屬性值設(shè)置為[NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession],而忽略了屬性值urlCredential。
6.- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;
為HTTP header設(shè)置value,用來追加到每個(gè)下載對應(yīng)的HTTP request, 若傳遞的value為nil,則將對應(yīng)的field移除。
擴(kuò)展里面定義了一個(gè)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è)置一個(gè)過濾器,為下載圖片的HTTP request選取header.意味著最終使用的headers是經(jīng)過這個(gè)block過濾之后的返回值。
9.- (void)setOperationClass:(Class)operationClass;
設(shè)置一個(gè)SDWebImageDownloaderOperation的子類 ,在每次 SDWebImage 構(gòu)建一個(gè)下載圖片的請求操作的時(shí)候作為默認(rèn)的NSOperation使用.
參數(shù)operationClass為要設(shè)置的默認(rèn)下載操作的SDWebImageDownloaderOperation的子類。 傳遞 nil 會恢復(fù)為SDWebImageDownloaderOperation。
以下兩個(gè)方法是下載控制方法了
- (id <SDWebImageOperation>)downloadImageWithURL: options: progress: completed:
這個(gè)方法用指定的URL創(chuàng)建一個(gè)異步下載實(shí)例。
有關(guān)completedBlock回調(diào)的一些解釋:下載完成的時(shí)候block會調(diào)用一次.
沒有使用SDWebImageDownloaderProgressiveDownload選項(xiàng)的情況下,如果下載成功會設(shè)置image參數(shù),如果出錯(cuò),會根據(jù)錯(cuò)誤設(shè)置error參數(shù). 最后一個(gè)參數(shù)總是YES. 如果使用了SDWebImageDownloaderProgressiveDownload選項(xiàng),這個(gè)block會使用部分image的對象有間隔地重復(fù)調(diào)用,同時(shí)finished參數(shù)設(shè)置為NO,直到使用完整的image對象和值為YES的finished參數(shù)進(jìn)行最后一次調(diào)用.如果出錯(cuò),finished參數(shù)總是YES.
- (void)setSuspended:(BOOL)suspended;
設(shè)置下載隊(duì)列的掛起(暫停)狀態(tài)。若為YES,隊(duì)列不再開啟新的下載操作,再向隊(duì)列里面添加的操作也不會被開啟,但是正在執(zhí)行的操作依然繼續(xù)執(zhí)行。
下面我們就來看一下下載方法的實(shí)現(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放到隊(duì)列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ò)訪問驗(yàn)證的憑據(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ù)下載選項(xiàng)SDWebImageDownloaderHighPriority設(shè)置優(yōu)先級
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
// 將操作加入到操作隊(duì)列 downloadQueue 中
// 如果是 LIFO 順序,則將新的操作作為原隊(duì)列中最后一個(gè)操作的依賴,然后將新操作設(shè)置為最后一個(gè)操作
[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;
}
重點(diǎn)就是addProgressCallback: completedBlock: forURL: createCallback:的執(zhí)行了,SDWebImageDownloader將外部傳來的進(jìn)度回調(diào)、完成回調(diào)、url直接傳遞給這個(gè)方法,并實(shí)現(xiàn)創(chuàng)建下載操作的代碼塊作為這個(gè)方法的createCallback參數(shù)值。下面就看一下這個(gè)方法的實(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ù)的解釋:
向分配隊(duì)列提交一個(gè)同步執(zhí)行的barrier block。與dispatch_barrier_async不同,這個(gè)函數(shù)直到barrier block執(zhí)行完畢才會返回,在當(dāng)前隊(duì)列調(diào)用這個(gè)函數(shù)會導(dǎo)致死鎖。當(dāng)barrier block被放進(jìn)一個(gè)私有的并行隊(duì)列后,它不會被立刻執(zhí)行。實(shí)際為,隊(duì)列會等待直到當(dāng)前正在執(zhí)行的blocks執(zhí)行完畢。到那個(gè)時(shí)刻,隊(duì)列才會自己執(zhí)行barrier block。而任何放到 barrier block之后的block直到 barrier block執(zhí)行完畢才會執(zhí)行。
傳遞的隊(duì)列參數(shù)應(yīng)該是你自己用dispatch_queue_create函數(shù)創(chuàng)建的一個(gè)并行隊(duì)列。如果你傳遞一個(gè)串行隊(duì)列或者全局并行隊(duì)列,這個(gè)函數(shù)的行為和 dispatch_sync相同。
與dispatch_barrier_async不同,它不會對目標(biāo)隊(duì)列進(jìn)行強(qiáng)引用(retain操作)。因?yàn)檎{(diào)用這個(gè)方法是同步的,它“借用”了調(diào)用者的引用。而且,沒有對block進(jìn)行Block_copy操作。
作為對其優(yōu)化,這個(gè)函數(shù)會在可能的情況下在當(dāng)前線程喚起barrier block。
*/
// 為確保不會死鎖,當(dāng)前隊(duì)列是另一個(gè)隊(duì)列,而不能是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只包含著一個(gè)元素,這個(gè)元素是一個(gè)NSMutableDictionary類型,它的key為NSString代表著回調(diào)類型,value為block,是對應(yīng)的回調(diào)
*/
// 同一時(shí)刻對相同url的多個(gè)下載請求只進(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),也就是第一次使用這個(gè)url創(chuàng)建下載任務(wù),則執(zhí)行一次創(chuàng)建回調(diào)。
在創(chuàng)建回調(diào)中創(chuàng)建下載操作,dispatch_barrier_sync執(zhí)行確保同一時(shí)間只有一個(gè)線程操作URLCallbacks屬性,也就是確保了下面創(chuàng)建過程中在給operation傳遞回調(diào)的時(shí)候能取到正確的self.URLCallbacks[url]值。同時(shí)保證后面有相同的url再次創(chuàng)建時(shí),if (!self.URLCallbacks[url])分支不再進(jìn)入,first==NO,也就不再繼續(xù)調(diào)用創(chuàng)建回調(diào)。這樣就確保了同一個(gè)url對應(yīng)的圖片不會被重復(fù)下載。
而下載器的完成回調(diào)中會將url從self.URLCallbacks中remove,雖然remove掉了,但是再次使用這個(gè)url進(jìn)行下載圖片的時(shí)候,Manager會向緩存中讀取下載成功的圖片了,而不是無腦地直接添加下載任務(wù);即使之前的下載是失敗的(也就是說沒有緩存),這樣繼續(xù)添加下載任務(wù)也是合情合理的。
// 因此準(zhǔn)確地說,將這個(gè)block放到并行隊(duì)列dispatch_barrier_sync執(zhí)行確保了,同一個(gè)url的圖片不會同一時(shí)刻進(jìn)行多次下載.
// 這樣做還使得下載操作的創(chuàng)建同步進(jìn)行,因?yàn)橐粋€(gè)新的下載操作還沒有創(chuàng)建完成,self.barrierQueue會繼續(xù)等待它完成,然后才能執(zhí)行下一個(gè)添加下載任務(wù)的block。所以說SD添加下載任務(wù)是同步的,而且都是在self.barrierQueue這個(gè)并行隊(duì)列中,同步添加任務(wù)。這樣也保證了根據(jù)executionOrder設(shè)置依賴關(guān)是正確的。換句話說如果創(chuàng)建下載任務(wù)不是使用dispatch_barrier_sync完成的,而是使用異步方法 ,雖然依次添加創(chuàng)建下載操作A、B、C的任務(wù),但實(shí)際創(chuàng)建順序可能為A、C、B,這樣當(dāng)executionOrder的值是SDWebImageDownloaderLIFOExecutionOrder,設(shè)置的操作依賴關(guān)系就變成了A依賴C,C依賴B
// 但是添加之后的下載依然是在下載隊(duì)列downloadQueue中異步執(zhí)行,絲毫不會影響到下載效率。
// 以上就是說了SD下載的關(guān)鍵點(diǎn):創(chuàng)建下載任務(wù)在barrierQueue隊(duì)列中,執(zhí)行下載在downloadQueue隊(duì)列中。
*/
}
});
}
關(guān)于SDWebImage的源碼閱讀就到這里結(jié)束,有什么不對的地方,歡迎指正。
相關(guān)資料:
通讀SDWebImage①--總體梳理、下載和緩存
SDWebImage 源碼閱讀筆記(三)