在SDWebImage源碼解析(一)中图张,我從宏觀上介紹了SDWebImage項(xiàng)目赠橙,并詳細(xì)介紹了UIImageView+WebCache
和SDWebImageManager
兩個(gè)類〈细唬現(xiàn)在我們繼續(xù)研究SDWebImageDownloader
和SDImageCache
肝谭。
SDWebImageDownloader
Asynchronous downloader dedicated and optimized for image loading.
SDWebImageDownloader
是專用的且優(yōu)化的圖片異步加載器特笋。先了解一下下載選項(xiàng):
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
//默認(rèn)的使用模式,前往下載,返回進(jìn)度block信息,完成時(shí)調(diào)用completedBlock
SDWebImageDownloaderLowPriority = 1 << 0,
// 漸進(jìn)式下載,如果設(shè)置了這個(gè)選項(xiàng),會(huì)在下載過程中,每次接收到一段返回?cái)?shù)據(jù)就會(huì)調(diào)用一次完成回調(diào),回調(diào)中的image參數(shù)為未下載完成的部分圖像,可以實(shí)現(xiàn)將圖片一點(diǎn)點(diǎn)顯示出來的功能
SDWebImageDownloaderProgressiveDownload = 1 << 1,
// 默認(rèn)情況下請(qǐng)求不使用NSURLCache甘有,如果設(shè)置該選項(xiàng)律适,則以默認(rèn)的緩存策略來使用NSURLCache
SDWebImageDownloaderUseNSURLCache = 1 << 2,
// 如果從NSURLcache緩存中讀取圖片腔长,則在調(diào)用完成block的時(shí)候,傳遞空的image或者imageData
SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
// 在iOS 4+系統(tǒng)上袭祟,允許程序進(jìn)入后臺(tái)后繼續(xù)下載圖片。該操作通過向系統(tǒng)申請(qǐng)額外時(shí)間來完成后臺(tái)下載捞附。如果后臺(tái)任務(wù)終止巾乳,則操作將被取消
SDWebImageDownloaderContinueInBackground = 1 << 4,
//通過設(shè)置NSMutableURLRequest.HTTPShouldHandleCookies = YES來處理存儲(chǔ)在NSHTTPCookieStore中的cookie
SDWebImageDownloaderHandleCookies = 1 << 5,
// 允許不受信任的SSL證書。主要用于測(cè)試目的(生產(chǎn)環(huán)境慎用)
SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
// 將圖片下載放到高優(yōu)先級(jí)隊(duì)列中
SDWebImageDownloaderHighPriority = 1 << 7,
};
再看看下載順序:
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
//默認(rèn)的下載順序鸟召,先進(jìn)先出
SDWebImageDownloaderFIFOExecutionOrder,
//后進(jìn)先出
SDWebImageDownloaderLIFOExecutionOrder
};
SDWebImageDownloader
也定義了三個(gè)block:
// 下載進(jìn)度回調(diào)(返回已經(jīng)接收的圖片數(shù)據(jù)的大小,未接收的圖片數(shù)據(jù)的大小)
typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
// 下載完成回調(diào)胆绊,返回圖片數(shù)據(jù)或錯(cuò)誤
typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
//過濾HTTP請(qǐng)求的Header
typedef NSDictionary *(^SDWebImageDownloaderHeadersFilterBlock)(NSURL *url, NSDictionary *headers);
類方法分別是:
//給每個(gè)HTTP下載請(qǐng)求頭的指定field設(shè)置值。
- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;
//返回HTTP特定field的值
- (NSString *)valueForHTTPHeaderField:(NSString *)field;
//設(shè)置一個(gè)SDWebImageDownloaderOperation的子類作為下載請(qǐng)求的默認(rèn)NSOperation
- (void)setOperationClass:(Class)operationClass;
//創(chuàng)建一個(gè)SDWebImageDownloader異步下載實(shí)例欧募,圖片下載完成或錯(cuò)誤時(shí)压状,通知delegate回調(diào)。方法返回一個(gè) SDWebImageOperation
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
// 設(shè)置下載隊(duì)列為掛起狀態(tài)
- (void)setSuspended:(BOOL)suspended;
//取消隊(duì)列中的所有操作跟继。
- (void)cancelAllDownloads;
實(shí)際上种冬,SDWebImageDownloader
管理一個(gè)下載隊(duì)列downloadQueue
,默認(rèn)最大的并行操作個(gè)數(shù)是6舔糖。隊(duì)列中每一個(gè)SDWebImageDownloaderOperation
實(shí)例才是真正的下載請(qǐng)求執(zhí)行者娱两。
我們重點(diǎn)研究核心下載方法
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
該方法就是調(diào)用了另外一個(gè)關(guān)鍵方法:
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(NSURL *)url
createCallback:(SDWebImageNoParamsBlock)createCallback {
// url作為URLCallbacks的key,如果為nil ,直接調(diào)用completedBlock金吗。
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return;
}
//將所有下載任務(wù)的網(wǎng)絡(luò)響應(yīng)處理放到barrierQueue隊(duì)列中十兢。
//并設(shè)置柵欄來確保同一時(shí)間只有一個(gè)線程操作URLCallbacks屬性
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
//修改url對(duì)應(yīng)的URLCallbacks
//URLCallbacks是一個(gè)字典: key是url, value是數(shù)組
//數(shù)組的元素是字典趣竣,key是callback類型字符串,value是callback的block
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;
//第一次請(qǐng)求這個(gè)url 才去真正做http請(qǐng)求
if (first) {
createCallback();
}
});
}
該方法為下載的操作添加回調(diào)的塊, 在下載進(jìn)行時(shí), 或者在下載結(jié)束時(shí)執(zhí)行一些操作旱物。圖片下載的progressBlock
和completedBlock
回調(diào)由一個(gè)字典URLCallbacks
管理遥缕。字典的key是圖片的url,value 是一個(gè)數(shù)組宵呛,數(shù)組只包含一個(gè)元素,這個(gè)元素的類型是NSMutableDictionary類型,這個(gè)字典的key為NSString類型代表著回調(diào)類型,value為block,是對(duì)應(yīng)的回調(diào)通砍。由于允許多個(gè)圖片同時(shí)下載,因此可能會(huì)有多個(gè)線程同時(shí)操作URLCallbacks屬性烤蜕。為了保證線程安全封孙,將下載操作作為一個(gè)個(gè)任務(wù)放到barrierQueue隊(duì)列中,并設(shè)置柵欄來確保同一時(shí)間只有一個(gè)線程操作URLCallbacks屬性
兩個(gè)回調(diào)對(duì)應(yīng)的key分別是
static NSString *const kProgressCallbackKey = @"progress";
static NSString *const kCompletedCallbackKey = @"completed";
如果URLCallbacks
沒有url這個(gè)key讽营,說明是第一次請(qǐng)求這個(gè)url虎忌,需要調(diào)用createCallback創(chuàng)建下載任務(wù),即使用
- (id)initWithRequest:(NSURLRequest *)request
inSession:(NSURLSession *)session
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock
cancelled:(SDWebImageNoParamsBlock)cancelBlock
初始化SDWebImageDownloaderOperation實(shí)例橱鹏。
下載任務(wù)使用NSMutableURLRequest膜蠢,默認(rèn)超時(shí)時(shí)間是15秒。
在progress block中我們?nèi)〕龃鎯?chǔ)在URLCallbacks中的progressBlock
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
});
for (NSDictionary *callbacks in callbacksForURL) {
//異步提交莉兰, 當(dāng)前線程直接返回
//callbacks在main_queue中并行執(zhí)行
dispatch_async(dispatch_get_main_queue(), ^{
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
if (callback)
callback(receivedSize, expectedSize);
});
}
對(duì)已經(jīng)接收到的大小和期待的大小調(diào)用callback挑围;
在completed block中我們?nèi)〕龃鎯?chǔ)在URLCallbacks中的completedBlock
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);
}
對(duì)image和data調(diào)用callback;
在cancelled block中,我們移除存儲(chǔ)在URLCallbacks的數(shù)組糖荒。
初始化完成后杉辙,再設(shè)置operation的參數(shù):
//是否解壓下載的圖片,默認(rèn)是YES,但是會(huì)消耗掉很多內(nèi)存捶朵,如果遇到內(nèi)存不足的crash時(shí)蜘矢,將值設(shè)為NO。
operation.shouldDecompressImages = wself.shouldDecompressImages;
//設(shè)置證書
if (wself.urlCredential) {
operation.credential = wself.urlCredential;
} else if (wself.username && wself.password) {
operation.credential = [NSURLCredential credentialWithUser:wself.username
password:wself.password
persistence:NSURLCredentialPersistenceForSession];
}
//設(shè)置隊(duì)列優(yōu)先級(jí)
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
最后將這個(gè)SDWebImageDownloaderOperation實(shí)例添加到downloadQueue隊(duì)列中去综看。如果下載執(zhí)行順序是LIFO品腹,還要加上任務(wù)的依賴
//加入操作隊(duì)列后, operation 真正開始執(zhí)行start
//所有的下載任務(wù)放在downloadQueue隊(duì)列中
[wself.downloadQueue addOperation:operation];
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
//加上任務(wù)的依賴红碑,也就是說依賴的任務(wù)都完成后舞吭,才能執(zhí)行當(dāng)前任務(wù)
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
SDWebImageDownloaderOperation
現(xiàn)在我們來研究一下上面提到的SDWebImageDownloaderOperation類。
SDWebImageDownloaderOperation是NSOperation的子類析珊,遵循SDWebImageOperation
羡鸥, NSURLSessionTaskDelegate
,NSURLSessionDataDelegate
協(xié)議唾琼,并重寫了start方法兄春。在start方法中真正處理HTTP請(qǐng)求和URL鏈接澎剥。
首先監(jiān)測(cè)下載狀態(tài):
//管理下載狀態(tài)锡溯,如果已取消赶舆,則重置當(dāng)前下載并設(shè)置完成狀態(tài)為YES
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
如果是iOS4.0以上的版本,還需要考慮是否在后臺(tái)執(zhí)行:
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
//如果設(shè)置了在后臺(tái)執(zhí)行祭饭,則進(jìn)行后臺(tái)執(zhí)行
__weak __typeof__ (self) wself = self;
UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
// 如果在系統(tǒng)規(guī)定時(shí)間內(nèi)任務(wù)還沒有完成(一般是10分鐘)芜茵,結(jié)束后臺(tái)任務(wù)
__strong __typeof (wself) sself = wself;
if (sself) {
[sself cancel];
[app endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
Version3.8中,下載已經(jīng)由原先的NSURLConnection切換到了NSURLSession了:
NSURLSession *session = self.unownedSession;
if (!self.unownedSession) {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
//為任務(wù)創(chuàng)建會(huì)話倡蝙,我們給delegateQueue設(shè)置nil來創(chuàng)建一個(gè)順序操作隊(duì)列去執(zhí)行所有的代理方法和完成回調(diào)九串。
self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
session = self.ownedSession;
}
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
self.thread = [NSThread currentThread];
創(chuàng)建好任務(wù)后開始執(zhí)行請(qǐng)求。 如果任務(wù)創(chuàng)建成功寺鸥,可能需要調(diào)用progressBlock回調(diào)并發(fā)送下載開始的通知猪钮;如果創(chuàng)建失敗,直接執(zhí)行完成回調(diào),并傳遞一個(gè)connection沒有初始化的錯(cuò)誤:
//開啟任務(wù)
[self.dataTask resume];
if (self.dataTask) {
if (self.progressBlock) {
self.progressBlock(0, NSURLResponseUnknownLength);
}
dispatch_async(dispatch_get_main_queue(), ^{
// 在主線程中發(fā)送開始下載的通知
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
} else {
//如果session創(chuàng)建失敗,直接執(zhí)行完成回調(diào),并傳遞一個(gè)connection沒有初始化的錯(cuò)誤
if (self.completedBlock) {
self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
}
}
任務(wù)開始后胆建,我們需要關(guān)注NSURLSessionDataDelegate的幾個(gè)代理方法烤低。
首先是
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler
此代理方法告訴delegate已經(jīng)接受到服務(wù)器的初始應(yīng)答, 準(zhǔn)備接下來的數(shù)據(jù)任務(wù)的操作。這里主要可講的是對(duì)返回碼為304的處理笆载。在HTTP的返回碼中扑馁,304表示服務(wù)端資源未改變,可直接使用客戶端未過期的資源凉驻,我們需要取消operation并返回緩存中的image腻要。
其次是
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
此代理方法告訴delegate已經(jīng)接收到部分?jǐn)?shù)據(jù),拼接數(shù)據(jù)涝登。
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
//添加新收到的部分?jǐn)?shù)據(jù)
[self.imageData appendData:data];
//如果SDWebImageDownloaderOptions選擇了逐步下載模式而且還在下載中雄家,需要實(shí)時(shí)更新下載的資源
if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
//已經(jīng)下載的總大小
const NSInteger totalSize = self.imageData.length;
// 利用現(xiàn)有的數(shù)據(jù)創(chuàng)建一個(gè)CGImageSourceRef對(duì)象
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
//首次進(jìn)入,從這些包含圖像信息的數(shù)據(jù)中取出圖像的長(zhǎng)胀滚、寬咳短、方向等信息以備使用
if (width + height == 0) {
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
if (properties) {
NSInteger orientationValue = -1;
CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
if (val) CFNumberGetValue(val, kCFNumberLongType, &height);
val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
if (val) CFNumberGetValue(val, kCFNumberLongType, &width);
val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);
CFRelease(properties);
//繪制到Core Graphics時(shí),會(huì)丟失方向信息蛛淋,這意味著有時(shí)候由initWithCGIImage創(chuàng)建的圖片 // 的方向會(huì)不對(duì)咙好,所以在這邊先保存這個(gè)信息并在后面使用
orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
}
}
//下載未完成
if (width + height > 0 && totalSize < self.expectedSize) {
// 使用現(xiàn)有的數(shù)據(jù)創(chuàng)建部分圖片對(duì)象,如果數(shù)據(jù)中存有多張圖片褐荷,則取第一張
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
#ifdef TARGET_OS_IPHONE
// Workaround for iOS anamorphic image
// 對(duì)下載下來的圖片做個(gè)顏色空間轉(zhuǎn)換等處理
if (partialImageRef) {
const size_t partialHeight = CGImageGetHeight(partialImageRef);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
CGColorSpaceRelease(colorSpace);
if (bmContext) {
CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef);
CGImageRelease(partialImageRef);
partialImageRef = CGBitmapContextCreateImage(bmContext);
CGContextRelease(bmContext);
}
else {
CGImageRelease(partialImageRef);
partialImageRef = nil;
}
}
#endif
if (partialImageRef) {
UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
// 對(duì)圖片進(jìn)行縮放
UIImage *scaledImage = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages) {
// 對(duì)圖片解壓縮
image = [UIImage decodedImageWithImage:scaledImage];
}
else {
image = scaledImage;
}
CGImageRelease(partialImageRef);
dispatch_main_sync_safe(^{
if (self.completedBlock) {
self.completedBlock(image, nil, nil, NO);
}
});
}
}
CFRelease(imageSource);
}
//調(diào)用progressBlock勾效,實(shí)時(shí)更新圖像信息
if (self.progressBlock) {
self.progressBlock(self.imageData.length, self.expectedSize);
}
}
另外還有NSURLSessionTaskDelegate的兩個(gè)代理方法:
//告訴delegate, task已經(jīng)完成,直接調(diào)用completedBlock叛甫,刷新UIImageView层宫。
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
//需要請(qǐng)求認(rèn)證
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
這里就不再一一贅述了。
SDImageCache
現(xiàn)在我們研究緩存部分其监,即SDImageCache類萌腿。
SDImageCache maintains a memory cache and an optional disk cache. Disk cache write operations are performed asynchronous so it doesn’t add unnecessary latency to the UI.
SDImageCache維持了一個(gè)內(nèi)存緩存memCache和一個(gè)可選的磁盤緩存fileManager,磁盤緩存的寫操作時(shí)異步的抖苦。
內(nèi)存緩存是用NSCache實(shí)現(xiàn)的毁菱,以Key-Value的形式存儲(chǔ)圖片米死,當(dāng)內(nèi)存不夠的時(shí)候會(huì)清除所有緩存圖片。磁盤緩存則是緩存到沙盒中贮庞,文件替換方式是以時(shí)間為單位峦筒,剔除時(shí)間大于一周的圖片文件。
先來看看幾個(gè)重要的屬性:
//同SDWebImageDownloader的屬性
//是否解壓下載的圖片窗慎,默認(rèn)是YES,但是會(huì)消耗掉很多內(nèi)存物喷,如果遇到內(nèi)存不足的crash時(shí),將值設(shè)為NO遮斥。
@property (assign, nonatomic) BOOL shouldDecompressImages;
//是否使用內(nèi)存緩存峦失,默認(rèn)YES
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
//內(nèi)存緩存的最大像素量
@property (assign, nonatomic) NSUInteger maxMemoryCost;
//內(nèi)存緩存的最大對(duì)象數(shù)
@property (assign, nonatomic) NSUInteger maxMemoryCountLimit;
//圖片在緩存中的最長(zhǎng)壽命,默認(rèn)1周术吗,超期刪除
@property (assign, nonatomic) NSInteger maxCacheAge;
//最大緩存大小
@property (assign, nonatomic) NSUInteger maxCacheSize;
再看看幾個(gè)重要的方法:
//用指定的命名空間來初始化一個(gè)cache
//創(chuàng)建磁盤緩存路徑宠进,調(diào)用initWithNamespace:diskCacheDirectory方法
- (id)initWithNamespace:(NSString *)ns;
//創(chuàng)建memCache和fileManager,初始化diskCachePath等屬性
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory;
//將key對(duì)應(yīng)的image存儲(chǔ)到內(nèi)存緩存和磁盤緩存中
- (void)storeImage:(UIImage *)image forKey:(NSString *)key;
//將key對(duì)應(yīng)的image存儲(chǔ)到內(nèi)存緩存藐翎,是否同時(shí)存入磁盤中由參數(shù)toDisk決定
- (void)storeImage:(UIImage *)image forKey:(NSString *)key toDisk:(BOOL)toDisk;
//功能同上材蹬,參數(shù)recalculate指明imageData是否可用或者應(yīng)該從UIImage重新構(gòu)造;參數(shù)imageData是由服務(wù)器返回吝镣,可以用于磁盤存儲(chǔ)堤器,這樣可以避免將image轉(zhuǎn)換為一個(gè)可存儲(chǔ)/壓縮的圖片以節(jié)省CPU。
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk;
//真正將key對(duì)應(yīng)的image存儲(chǔ)到磁盤緩存中
- (void)storeImageDataToDisk:(NSData *)imageData forKey:(NSString *)key;
//異步查詢disk cache
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
//同步查詢memory cache
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key;
//先檢測(cè)memory cache,再監(jiān)測(cè)disk cache并存到memory cache里
- (UIImage *)imageFromDiskCacheForKey:(NSString *)key;
//從memory cache 中刪除image末贾,并從disk cache中異步刪除
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion;
//清空memory cache闸溃,收到內(nèi)存警告時(shí)調(diào)用
- (void)clearMemory;
// 清空disk cache
- (void)clearDisk;
//清空disk cache ,非阻塞方法拱撵,立即返回
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
//清除disk cache中所有過期image
- (void)cleanDisk;
//清除disk cache中所有過期image辉川,非阻塞方法,立即返回
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock;
//同步獲取disk cache 使用的cache 大小拴测,利用NSFileManager的enumeratorAtPath方法遍歷disk cache文件累計(jì)fileSize
- (NSUInteger)getSize;
//同步獲取disk cache的圖片數(shù)量
- (NSUInteger)getDiskCount;
//異步獲取disk cache的圖片數(shù)量和緩存大小
- (void)calculateSizeWithCompletionBlock:(SDWebImageCalculateSizeBlock)completionBlock;
//監(jiān)測(cè)key對(duì)應(yīng)的圖片是否在disk cache中乓旗,方法先按照defaultCachePathForKey生成的path尋找,如果沒有則對(duì)path刪除擴(kuò)展名集索,再尋找屿愚。
- (BOOL)diskImageExistsWithKey:(NSString *)key;
//功能同上,異步的
- (void)diskImageExistsWithKey:(NSString *)key completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
//根據(jù)指定的key生成cache 路徑务荆,為disk cache 使用
- (NSString *)cachePathForKey:(NSString *)key inPath:(NSString *)path;
//指定的key默認(rèn)的cache 路徑妆距,調(diào)用上面的方法,第二個(gè)參數(shù)為self.diskCachePath
- (NSString *)defaultCachePathForKey:(NSString *)key;
disk cache的文件名是key做MD5后的字符串:
- (NSString *)cachedFileNameForKey:(NSString *)key {
const char *str = [key UTF8String];
if (str == NULL) {
str = "";
}
unsigned char r[CC_MD5_DIGEST_LENGTH];
CC_MD5(str, (CC_LONG)strlen(str), r);
NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
r[11], r[12], r[13], r[14], r[15], [[key pathExtension] isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", [key pathExtension]]];
return filename;
}
我們重點(diǎn)研究怎么存儲(chǔ)到緩存中的函匕,storeImage:forKey:
和storeImage:forKey:toDisk:
最終都是調(diào)用storeImage:recalculateFromImage:imageData:forKey:toDisk:
方法的娱据。
如果需要存儲(chǔ)到memory cache中,首先存入memory cache盅惜。
if (self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
如果需要存儲(chǔ)到disk cache中剩,在子線程中串行存儲(chǔ)到disk cache中:
dispatch_async(self.ioQueue, ^{//串行隊(duì)列
NSData *data = imageData;
if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
// 確定圖片是png還是jpeg. imageData為nil而且有alapha通道忌穿,當(dāng)作png處理
// PNG圖片的前八個(gè)字節(jié)是137 80 78 71 13 10 26 10
int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
BOOL imageIsPng = hasAlpha;
// 如果imageData有值,查看前綴
if ([imageData length] >= [kPNGSignatureData length]) {
imageIsPng = ImageDataHasPNGPreffix(imageData);
}
if (imageIsPng) {
// PNG
data = UIImagePNGRepresentation(image);
}
else {
//JPEGP
data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
}
#else
data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
}
//真正存儲(chǔ)到磁盤中
[self storeImageDataToDisk:data forKey:key];
});
最終真正存儲(chǔ)到磁盤中的方法是:
- (void)storeImageDataToDisk:(NSData *)imageData forKey:(NSString *)key {
//監(jiān)測(cè)imageData
if (!imageData) {
return;
}
//創(chuàng)建目錄
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
// 獲取默認(rèn)的緩存路徑
NSString *cachePathForKey = [self defaultCachePathForKey:key];
// 將路徑轉(zhuǎn)化為 NSUrl
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
//存儲(chǔ)文件
[_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
//禁用iCloud備份
if (self.shouldDisableiCloud) {
[fileURL setResourceValue:[NSNumber numberWithBool:YES] forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}
再來看看圖片查詢的幾個(gè)方法咽安。在SDWebImageManager中的downloadImageWithURL:options:progress:completed
方法中使用到了imageCache的queryDiskCacheForKey:done
方法伴网。這是SDImageCache里面查詢圖片的入口蓬推。
首先妆棒,從memory cache中查詢,如果找到圖片就直接使用并返回:
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
否則沸伏,去disk cache中查詢糕珊,同樣是在子線程的同步隊(duì)列中執(zhí)行。如果找到毅糟,還需要監(jiān)測(cè)是否需要存儲(chǔ)到memory cache中:
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);
});
}
});
其中红选,在memory cache 中查詢很簡(jiǎn)單,直接使用字典方法objectForKey
:
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
return [self.memCache objectForKey:key];
}
在disk cache中查詢姆另,需要根據(jù)key構(gòu)造各種可能的路徑喇肋。最后如果找到,需要縮放或者解壓縮:
// 構(gòu)造各種可能路徑去查詢
- (NSData *)diskImageDataBySearchingAllPathsForKey:(NSString *)key {
NSString *defaultPath = [self defaultCachePathForKey:key];
NSData *data = [NSData dataWithContentsOfFile:defaultPath];
if (data) {
return data;
}
// 考慮文件擴(kuò)展名
data = [NSData dataWithContentsOfFile:[defaultPath stringByDeletingPathExtension]];
if (data) {
return data;
}
NSArray *customPaths = [self.customPaths copy];
for (NSString *path in customPaths) {
NSString *filePath = [self cachePathForKey:key inPath:path];
NSData *imageData = [NSData dataWithContentsOfFile:filePath];
if (imageData) {
return imageData;
}
imageData = [NSData dataWithContentsOfFile:[filePath stringByDeletingPathExtension]];
if (imageData) {
return imageData;
}
}
return nil;
}
//從disk cache中查詢
- (UIImage *)diskImageForKey:(NSString *)key {
NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
if (data) {
UIImage *image = [UIImage sd_imageWithData:data];
//縮放
image = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages)
//解壓縮
image = [UIImage decodedImageWithImage:image];
}
return image;
}
else {
return nil;
}
}
最后來看看圖片的清理方式迹辐。移除指定key對(duì)應(yīng)的圖片有一系列方法蝶防,最終調(diào)用的方法是:
- (void)removeImageForKey:(NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(SDWebImageNoParamsBlock)completion {
if (key == nil) {
return;
}
//先從memory cache 中移除
if (self.shouldCacheImagesInMemory) {
[self.memCache removeObjectForKey:key];
}
//再?gòu)膁isk cache 中移除
if (fromDisk) {
dispatch_async(self.ioQueue, ^{
[_fileManager removeItemAtPath:[self defaultCachePathForKey:key] error:nil];
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
});
} else if (completion){
completion();
}
}
而清空cache有兩種方式,即完全清空與部分清空明吩。對(duì)于memory cache是完全清空的:
- (void)clearMemory {
[self.memCache removeAllObjects];
}
對(duì)于disk cache间学,兩種方式都有可能。完全清空的方式是直接把文件夾移除掉:
- (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion
{
dispatch_async(self.ioQueue, ^{
[_fileManager removeItemAtPath:self.diskCachePath error:nil];
[_fileManager createDirectoryAtPath:self.diskCachePath
withIntermediateDirectories:YES
attributes:nil
error:NULL];
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion();
});
}
});
}
部分清空是根據(jù)參數(shù)配置移除文件印荔,使文件的總大小小于最大使用空間低葫。清理策略有兩個(gè):
- 文件的緩存有效期:默認(rèn)是一周。如果文件的緩存時(shí)間超過這個(gè)時(shí)間值仍律,則將其移除嘿悬。
- 最大緩存空間大小:如果所有緩存文件的總大小超過最大緩存空間水泉,則會(huì)按照文件最后修改時(shí)間的逆序鹊漠,以每次一半的遞歸來移除那些過早的文件,直到緩存的實(shí)際大小小于我們?cè)O(shè)置的最大使用空間茶行。
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
// 通過文件的枚舉器來獲取緩存文件的有用的屬性
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
NSUInteger currentCacheSize = 0;
//遍歷cache 目錄躯概,刪除過期文件,存儲(chǔ)文件屬性
NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
//跳過文件夾
if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
//記錄待刪除的過期文件 并continue
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
//沒刪除的文件畔师,存儲(chǔ)文件的資源屬性 計(jì)算文件總大小
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
[cacheFiles setObject:resourceValues forKey:fileURL];
}
//刪除過期文件
for (NSURL *fileURL in urlsToDelete) {
[_fileManager removeItemAtURL:fileURL error:nil];
}
//剩下的cache總大小依然超出配置的cache最大值娶靡,執(zhí)行第二次清理
//首先清除最老的文件,每次清理一半看锉,遞歸
if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
// 所有文件按照修改時(shí)間排序
NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
//刪除文件 直到desiredCacheSize大小
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();
});
}
});
}
至此姿锭,我們已經(jīng)把SDWebImage最主要的幾個(gè)模塊分析清楚了塔鳍,我們可以繪制一個(gè)流程圖來對(duì)各個(gè)模塊的工作流做個(gè)總結(jié):
延伸
最后,我們延伸一點(diǎn)知識(shí)呻此,講講前面提到的dispatch_main_sync_safe
宏轮纫、dispatch_main_async_safe
宏以及SDWebImageDecoder
的作用。
這兩個(gè)宏比較簡(jiǎn)單焚鲜,直接看代碼:
#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);\
}
即保證當(dāng)前代碼在主線程中執(zhí)行掌唾,上面是同步調(diào)用,下面是異步調(diào)用忿磅。
判斷主線程的目的是避免出現(xiàn)死鎖問題:
*** 如果在主線程中執(zhí)行dispatch_sync(dispatch_get_main_queue(), block) 同步操作時(shí)糯彬,會(huì)出現(xiàn)死鎖問題,因?yàn)橹骶€程正在執(zhí)行當(dāng)前代碼葱她,根本無法將block添加到主隊(duì)列中 ***
SDWebImageDecoder
用來解壓縮圖片撩扒,關(guān)于為什么從磁盤讀取image后要做一次解壓縮,參考了v2panda的解釋吨些,僅供大家參考搓谆。
因?yàn)橥ㄟ^ imageNamed 創(chuàng)建 UIImage 時(shí),系統(tǒng)實(shí)際上只是在 Bundle 內(nèi)查找到文件名豪墅,然后把這個(gè)文件名放到 UIImage 里返回泉手,并沒有進(jìn)行實(shí)際的文件讀取和解碼。當(dāng) UIImage 第一次顯示到屏幕上時(shí)但校,其內(nèi)部的解碼方法才會(huì)被調(diào)用螃诅,同時(shí)解碼結(jié)果會(huì)保存到一個(gè)全局緩存去。在圖片解碼后状囱,App 第一次退到后臺(tái)和收到內(nèi)存警告時(shí)术裸,該圖片的緩存才會(huì)被清空,其他情況下緩存會(huì)一直存在亭枷。具體的說就是一個(gè)UIImage加載了jpeg或者png袭艺,當(dāng)UIImageView將要顯示這個(gè)UIImage的時(shí)候會(huì)先把png和jpeg解碼成未壓縮格式,所以SDWebImage有一個(gè)decodeImage方法叨粘,就是把這一步放在了異步線程做猾编,防止tableViewCell中的imageView加載圖片的時(shí)候在主線程解碼圖片,導(dǎo)致滑動(dòng)卡頓升敲。這樣效率很低答倡,但是只有瞬時(shí)的內(nèi)存需求。為了提高效率通過SDWebImageDecoder將包裝在Data下的資源解壓驴党,然后畫在另外一張圖片上瘪撇,這樣這張新圖片就不再需要重復(fù)解壓了,這種做法是典型的空間換時(shí)間的做法,如下從硬盤中去圖片時(shí),分別對(duì)圖片進(jìn)行了縮放和解壓縮操作倔既。