下載
下載管理器 ?SDWebImageDownLoader作為一個單例來管理圖片的下載操作脉漏。圖片的下載是放在一個NSOperationQueue操作隊列中完成。
所有下載操作的網絡響應序列化處理是放在一個自定義的并行調度隊列barrierQueue中來處理切距。
每個圖片的下載都對應一些回調操作,如下載進度惨远、下載完成等谜悟,這些操作是以block形式呈現的。
// 下載進度
typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
// 下載完成
typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
// Header過濾
typedef NSDictionary *(^SDWebImageDownloaderHeadersFilterBlock)(NSURL *url, NSDictionary *headers);
重點:圖片下載的回調信息是存放在SDWebImageDownLoader的URLCallbacks屬性中锨络。其中赌躺,key是圖片的url,value是一個數組羡儿,包含了圖片的多個回調信息礼患。由于,sd是支持多圖片下載的掠归,所以就有可能存在多個線程同時操作URLCallbacks缅叠,從而引發(fā)線程安全問題,為了保證線程安全虏冻,sd將這些操作作為一個個任務放到barrierQueue隊列中肤粱,并設置屏障(dispatch_barreer_sync)保證同一時間只有一個線程來操作URLCallbacks。
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
...
// 1. 以dispatch_barrier_sync操作來保證同一時間只有一個線程能對URLCallbacks進行操作
dispatch_barrier_sync(self.barrierQueue, ^{
...
// 2. 處理同一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;
...
});
}
整個下載管理器對于下載請求的管理都是放在downloadImageWithURL:options:progress:completed:方法里面來處理的厨相,該方法調用了上面所提到的addProgressCallback:andCompletedBlock:forURL:createCallback:方法來將請求的信息存入管理器中领曼,同時在創(chuàng)建回調的block中創(chuàng)建新的操作,配置之后將其放入downloadQueue操作隊列中蛮穿,最后方法返回新創(chuàng)建的操作.具體如下:
- (id)downloadImageWithURL:(NSURL*)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
...
[selfaddProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{
...
// 1. 創(chuàng)建請求對象庶骄,并根據options參數設置其屬性
// 為了避免潛在的重復緩存(NSURLCache + SDImageCache),如果沒有明確告知需要緩存践磅,則禁用圖片請求的緩存操作
NSMutableURLRequest*request = [[NSMutableURLRequestalloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ?NSURLRequestUseProtocolCachePolicy:NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
...
// 2. 創(chuàng)建SDWebImageDownloaderOperation操作對象单刁,并進行配置
// 配置信息包括是否需要認證、優(yōu)先級
operation = [[wself.operationClass alloc] initWithRequest:request
options:options
progress:^(NSIntegerreceivedSize,NSIntegerexpectedSize) {
// 3. 從管理器的callbacksForURL中找出該URL所有的進度處理回調并調用
...
for(NSDictionary*callbacks in callbacksForURL) {
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
if(callback) callback(receivedSize, expectedSize);
}
}
completed:^(UIImage *image,NSData*data,NSError*error,BOOLfinished) {
// 4. 從管理器的callbacksForURL中找出該URL所有的完成處理回調并調用府适,
// 如果finished為YES羔飞,則將該url對應的回調信息從URLCallbacks中刪除
...
if(finished) {
[sself removeCallbacksForURL:url];
}
for(NSDictionary*callbacks in callbacksForURL) {
SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
if(callback) callback(image, data, error, finished);
}
}
cancelled:^{
// 5. 取消操作將該url對應的回調信息從URLCallbacks中刪除
SDWebImageDownloader *sself = wself;
if(!sself)return;
[sself removeCallbacksForURL:url];
}];
...
// 6. 將操作加入到操作隊列downloadQueue中
// 如果是LIFO順序,則將新的操作作為原隊列中最后一個操作的依賴檐春,然后將新操作設置為最后一個操作
[wself.downloadQueue addOperation:operation];
if(wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
}];
returnoperation;
}
下載操作 ? 每個圖片的下載都是一個Operation操作逻淌。? SDWebImageOperation 作為圖片下載操作的基礎協(xié)議。它只聲明了一個cancel方法疟暖,用于取消操作恍风。? 對于圖片的下載,SDWebImageDownloaderOperation完全依賴于URL加載系統(tǒng)中的NSURLConnection類誓篱。-connection:didReceiveData:該方法的主要任務是接收數據朋贬。每次接收到數據時,都會用現有的數據創(chuàng)建一個CGImageSourceRef對象以做處理窜骄。在首次獲取到數據時(width+height==0)會從這些包含圖像信息的數據中取出圖像的長锦募、寬、方向等信息以備使用邻遏。而后在圖片下載完成之前糠亩,會使用CGImageSourceRef對象創(chuàng)建一個圖片對象,經過縮放准验、解壓縮操作后生成一個UIImage對象供完成回調使用赎线。當然,在這個方法中還需要處理的就是進度信息糊饱。如果我們有設置進度回調的話垂寥,就調用這個進度回調以處理當前圖片的下載進度? ? ? ? (注:縮放操作可以查看SDWebImageCompat文件中的SDScaledImageForKey函數;解壓縮操作可以查看SDWebImageDecoder文件+decodedImageWithImage方法)
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
// 1. 附加數據
[self.imageData appendData:data];
if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
// 2. 獲取已下載數據總大小
const NSInteger totalSize = self.imageData.length;
// 3. 更新數據源另锋,我們需要傳入所有數據滞项,而不僅僅是新數據
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
// 4. 首次獲取到數據時,從這些數據中獲取圖片的長夭坪、寬文判、方向屬性值
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);
...
CFRelease(properties);
// 5. 當繪制到Core Graphics時,我們會丟失方向信息室梅,這意味著有時候由initWithCGIImage創(chuàng)建的圖片
//? 的方向會不對戏仓,所以在這邊我們先保存這個信息并在后面使用。
orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
}
}
// 6. 圖片還未下載完成
if (width + height > 0 && totalSize < self.expectedSize) {
// 7. 使用現有的數據創(chuàng)建圖片對象亡鼠,如果數據中存有多張圖片赏殃,則取第一張
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
#ifdef TARGET_OS_IPHONE
// 8. 適用于iOS變形圖像的解決方案。我的理解是由于iOS只支持RGB顏色空間拆宛,所以在此對下載下來的圖片做個顏色空間轉換處理嗓奢。
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
// 9. 對圖片進行縮放、解碼操作
if (partialImageRef) {
UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
UIImage *scaledImage = [self scaledImageForKey:key image:image];
image = [UIImage decodedImageWithImage:scaledImage];
CGImageRelease(partialImageRef);
dispatch_main_sync_safe(^{
if (self.completedBlock) {
self.completedBlock(image, nil, nil, NO);
}
});
}
}
CFRelease(imageSource);
}
if (self.progressBlock) {
self.progressBlock(self.imageData.length, self.expectedSize);
}
}
SDWebImageDownloaderOperation類是繼承自NSOperation類浑厚。它沒有簡單的實現main方法股耽,而是采用更加靈活的start方法,以便自己管理下載的狀態(tài)钳幅。在start方法中物蝙,創(chuàng)建了我們下載所使用的NSURLConnection對象,開啟了圖片的下載敢艰,同時拋出一個下載開始的通知诬乞。當然,如果我們期望下載在后臺處理,則只需要配置我們的下載選項震嫉,使其包含SDWebImageDownloaderContinueInBackground選項森瘪。start方法的具體實現如下:
- (void)start {
@synchronized (self) {
// 管理下載狀態(tài),如果已取消票堵,則重置當前下載并設置完成狀態(tài)為YES
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
// 1. 如果設置了在后臺執(zhí)行扼睬,則進行后臺執(zhí)行
if ([self shouldContinueWhenAppEntersBackground]) {
__weak __typeof__ (self) wself = self;
self.backgroundTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
...
}
}];
}
#endif
self.executing = YES;
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
self.thread = [NSThread currentThread];
}
[self.connection start];
if (self.connection) {
if (self.progressBlock) {
self.progressBlock(0, NSURLResponseUnknownLength);
}
// 2. 在主線程拋出下載開始通知
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
// 3. 啟動run loop
if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
}
else {
CFRunLoopRun();
}
// 4. 如果未完成,則取消連接
if (!self.isFinished) {
[self.connection cancel];
[self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
}
}
else {
...
}
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
[[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
#endif
}
在下載完成或下載失敗后悴势,需要停止當前線程的run loop窗宇,清除連接,并拋出下載停止的通知特纤。如果下載成功军俊,則會處理完整的圖片數據,對其進行適當的縮放與解壓縮操作捧存,以提供給完成回調使用粪躬。具體可參考-connectionDidFinishLoading:與-connection:didFailWithError:的實現。
緩存
為了減少網絡流量的消耗矗蕊,我們都希望下載下來的圖片緩存到本地短蜕,下次再去獲取同一張圖片時,可以直接從本地獲取傻咖,而不再從遠程服務器獲取朋魔。這樣做的另一個好處是提升了用戶體驗,用戶第二次查看同一幅圖片時卿操,能快速從本地獲取圖片直接呈現給用戶警检。 SDWebImage提供了對圖片緩存的支持,而該功能是由SDImageCache類來完成的害淤。該類負責處理內存緩存及一個可選的磁盤緩存扇雕。其中磁盤緩存的寫操作是異步的,這樣就不會對UI操作造成影響
內存緩存及磁盤緩? ? 內存緩存的處理是使用NSCache對象來實現的(NSCache是一個類似于集合的容器窥摄。它存儲key-value對镶奉,這一點類似于NSDictionary類。我們通常用使用緩存來臨時存儲短時間使用但創(chuàng)建昂貴的對象崭放。重用這些對象可以優(yōu)化性能哨苛,因為它們的值不需要重新計算。另外一方面币砂,這些對象對于程序來說不是緊要的建峭,在內存緊張時會被丟棄。)决摧。磁盤緩存的處理則是使用NSFileManager對象來實現的亿蒸。圖片存儲的位置是位于Cache文件夾凑兰。另外,SDImageCache還定義了一個串行隊列边锁,來異步存儲圖片
SDImageCache提供了大量方法來緩存姑食、獲取、移除及清空圖片砚蓬。而對于每個圖片矢门,為了方便地在內存或磁盤中對它進行這些操作,我們需要一個key值來索引它灰蛙。在內存中,我們將其作為NSCache的key值隔躲,而在磁盤中摩梧,我們用這個key作為圖片的文件名。對于一個遠程服務器下載的圖片宣旱,其url是作為這個key的最佳選擇了仅父。
存儲圖片? 該操作會在內存中放置一份緩存,而如果確定需要緩存到磁盤浑吟,則將磁盤緩存操作作為一個task放到串行隊列中處理笙纤。在iOS中,會先檢測圖片是PNG還是JPEG组力,并將其轉換為相應的圖片數據省容,最后將數據寫入到磁盤中(文件名是對key值做MD5摘要后的串)。緩存操作的基礎方法是-storeImage:recalculateFromImage:imageData:forKey:toDisk燎字,它的具體實現如下:
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
...
// 1. 內存緩存腥椒,將其存入NSCache中,同時傳入圖片的消耗值
[self.memCache setObject:image forKey:key cost:image.size.height * image.size.width * image.scale * image.scale];
if (toDisk) {
// 2. 如果確定需要磁盤緩存候衍,則將緩存操作作為一個任務放入ioQueue中
dispatch_async(self.ioQueue, ^{
NSData *data = imageData;
if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
// 3. 需要確定圖片是PNG還是JPEG笼蛛。PNG圖片容易檢測,因為有一個唯一簽名蛉鹿。PNG圖像的前8個字節(jié)總是包含以下值:137 80 78 71 13 10 26 10
// 在imageData為nil的情況下假定圖像為PNG滨砍。我們將其當作PNG以避免丟失透明度。而當有圖片數據時妖异,我們檢測其前綴惋戏,確定圖片的類型
BOOL imageIsPng = YES;
if ([imageData length] >= [kPNGSignatureData length]) {
imageIsPng = ImageDataHasPNGPreffix(imageData);
}
if (imageIsPng) {
data = UIImagePNGRepresentation(image);
}
else {
data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
}
#else
data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
}
// 4. 創(chuàng)建緩存文件并存儲圖片
if (data) {
if (![_fileManager fileExistsAtPath:_diskCachePath]) {
[_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}
[_fileManager createFileAtPath:[self defaultCachePathForKey:key] contents:data attributes:nil];
}
});
}
}
查詢圖片? 在內存或磁盤中查詢是否有key指定的圖片,則可以分別使用以下方法: ? - (UIImage *)imageFromMemoryCacheForKey:(NSString *)key; ? ? ? ? ? ? - (UIImage *)imageFromDiskCacheForKey:(NSString *)key; ? ? ?如果只是想查看本地是否存在key指定的圖片随闺,則不管是在內存還是在磁盤上日川,則可以使用以下方法:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
...
// 1. 首先查看內存緩存,如果查找到矩乐,則直接回調doneBlock并返回
UIImage *image = [self imageFromDiskCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
// 2. 如果內存中沒有龄句,則在磁盤中查找回论。如果找到,則將其放到內存緩存分歇,并調用doneBlock回調
NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
return;
}
@autoreleasepool {
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage) {
CGFloat cost = diskImage.size.height * diskImage.size.width * diskImage.scale * diskImage.scale;
[self.memCache setObject:diskImage forKey:key cost:cost];
}
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, SDImageCacheTypeDisk);
})
}
});
return operation;
}
移除圖片
磁盤緩存圖片的清理操作可以分為完全清空和部分清理傀蓉。完全清空操作是直接把緩存的文件夾移除,清空操作有以下兩個方法:
// ? ?- (void)clearDisk;
//? ? - (void)clearDiskOnCompletion:(SDWebImageNoParamsBlock)completion;
而部分清理則是根據我們設定的一些參數值來移除一些文件职抡,這里主要有兩個指標:文件的緩存有效期及最大緩存空間大小葬燎。文件的緩存有效期可以通過maxCacheAge屬性來設置,默認是1周的時間缚甩。如果文件的緩存時間超過這個時間值谱净,則將其移除。而最大緩存空間大小是通過maxCacheSize屬性來設置的擅威,如果所有緩存文件的總大小超過這一大小壕探,則會按照文件最后修改時間的逆序,以每次一半的遞歸來移除那些過早的文件郊丛,直到緩存的實際大小小于我們設置的最大使用空間李请。清理的操作在-cleanDiskWithCompletionBlock:方法中,其實現如下:
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];
// 1. 該枚舉器預先獲取緩存文件的有用的屬性
NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
NSUInteger currentCacheSize = 0;
// 2. 枚舉緩存文件夾中所有文件厉熟,該迭代有兩個目的:移除比過期日期更老的文件导盅;存儲文件屬性以備后面執(zhí)行基于緩存大小的清理操作
NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];
// 3. 跳過文件夾
if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// 4. 移除早于有效期的老文件
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
// 5. 存儲文件的引用并計算所有文件的總大小,以備后用
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
[cacheFiles setObject:resourceValues forKey:fileURL];
}
for (NSURL *fileURL in urlsToDelete) {
[_fileManager removeItemAtURL:fileURL error:nil];
}
// 6.如果磁盤緩存的大小大于我們配置的最大大小揍瑟,則執(zhí)行基于文件大小的清理白翻,我們首先刪除最老的文件
if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
// 7. 以設置的最大緩存大小的一半作為清理目標
const NSUInteger desiredCacheSize = self.maxCacheSize / 2;
// 8. 按照最后修改時間來排序剩下的緩存文件
NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
// 9. 刪除文件,直到緩存總大小降到我們期望的大小
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();
});
}
});
}
以上分析了圖片緩存操作月培,當然嘁字,除了上面的幾個操作,SDImageCache類還提供了一些輔助方法杉畜。如獲取緩存大小纪蜒、緩存中圖片的數量、判斷緩存中是否存在某個key指定的圖片此叠。另外纯续,SDImageCache類提供了一個單例方法的實現,所以我們可以將其當作單例對象來處理灭袁。
//? ? 匯總一些常用接口猬错、屬性:
//? ? -getSize? :獲得硬盤緩存的大小
//? ? -getDiskCount : 獲得硬盤緩存的圖片數量
//? ? -clearMemory? : 清理所有內存圖片
//? ? - removeImageForKey:(NSString *)key? 系列的方法 : 從內存、硬盤按要求指定清除圖片
//? ? maxMemoryCost? :? 保存在存儲器中像素的總和
//? ? maxCacheSize? :? 最大緩存大小 以字節(jié)為單位茸歧。默認沒有設置倦炒,也就是為0,而清理磁盤緩存的先決條件為self.maxCacheSize > 0软瞎,所以0表示無限制逢唤。
//? ? maxCacheAge : 在內存緩存保留的最長時間以秒為單位計算拉讯,默認是一周
SDWebImageManager
在實際的運用中,我們并不直接使用SDWebImageDownloader類及SDImageCache類來執(zhí)行圖片的下載及緩存鳖藕。為了方便用戶的使用魔慷,SDWebImage提供了SDWebImageManager對象來管理圖片的下載與緩存。而且我們經常用到的諸如UIImageView+WebCache等控件的分類都是基于SDWebImageManager對象的著恩。該對象將一個下載器和一個圖片緩存綁定在一起院尔,并對外提供兩個只讀屬性來獲取它們,如下代碼所示:
@interface SDWebImageManager : NSObject? ? ? ?
@property (weak, nonatomic) iddelegate;
@property (strong, nonatomic, readonly) SDImageCache *imageCache;
@property (strong, nonatomic, readonly) SDWebImageDownloader *imageDownloader;
...
@end
從上面的代碼中我們還可以看到有一個delegate屬性喉誊,其是一個id對象邀摆。SDWebImageManagerDelegate聲明了兩個可選實現的方法,如下所示:
// 控制當圖片在緩存中沒有找到時裹驰,應該下載哪個圖片
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;
// 允許在圖片已經被下載完成且被緩存到磁盤或內存前立即轉換
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;
這兩個代理方法會在SDWebImageManager的-downloadImageWithURL:options:progress:completed:方法中調用隧熙,而這個方法是SDWebImageManager類的核心所在。我們來看看它的具體實現:
- (id)downloadImageWithURL:(NSURL *)urloptions:(SDWebImageOptions)optionsprogress:(SDWebImageDownloaderProgressBlock)progressBlockcompleted:(SDWebImageCompletionWithFinishedBlock)completedBlock {? ??
...? ??
// 前面省略n行幻林。主要作了如下處理:? ?
?// 1. 判斷url的合法性? ??
// 2. 創(chuàng)建SDWebImageCombinedOperation對象? ??
// 3. 查看url是否是之前下載失敗過的 ??
// 4. 如果url為nil,或者在不可重試的情況下是一個下載失敗過的url音念,則直接返回操作對象并調用完成回調? ??
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
? ? ? ? ...? ? ? ?
?if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {? ? ? ? ? ??
// 下載? ? ? ? ? ??
idsubOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
if (weakOperation.isCancelled) {
// 操作被取消沪饺,則不做任務事情
}
else if (error) {
// 如果出錯,則調用完成回調闷愤,并將url放入下載挫敗url數組中
...
}
else {
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
if (options & SDWebImageRefreshCached && image && !downloadedImage) {
// Image refresh hit the NSURLCache cache, do not call the completion block
}
else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
// 在全局隊列中并行處理圖片的緩存
// 首先對圖片做個轉換操作整葡,該操作是代理對象實現的
// 然后對圖片做緩存處理
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
[self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
}
...
});
}
else {
if (downloadedImage && finished) {
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
}
...
}
}
// 下載完成并緩存后,將操作從隊列中移除
if (finished) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
}];
// 設置取消回調
operation.cancelBlock = ^{
[subOperation cancel];
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:weakOperation];
}
};
}
else if (image) {
...
}
else {
...
}
}];
return operation;
}
對于這個方法讥脐,我們沒有做過多的解釋遭居。其主要就是下載圖片并根據操作選項來緩存圖片。上面這個下載方法中的操作選項參數是由枚舉SDWebImageOptions來定義的旬渠,這個操作中的一些選項是與SDWebImageDownloaderOptions中的選項對應的俱萍。我們來看看這個SDWebImageOptions選項都有哪些:
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
// 默認情況下,當URL下載失敗時告丢,URL會被列入黑名單枪蘑,導致庫不會再去重試,該標記用于禁用黑名單
SDWebImageRetryFailed = 1 << 0,
// 默認情況下岖免,圖片下載開始于UI交互岳颇,該標記禁用這一特性,這樣下載延遲到UIScrollView減速時
SDWebImageLowPriority = 1 << 1,
// 該標記禁用磁盤緩存
SDWebImageCacheMemoryOnly = 1 << 2,
// 該標記啟用漸進式下載颅湘,圖片在下載過程中是漸漸顯示的话侧,如同瀏覽器一下。
// 默認情況下闯参,圖像在下載完成后一次性顯示
SDWebImageProgressiveDownload = 1 << 3,
// 即使圖片緩存了瞻鹏,也期望HTTP響應cache control悲立,并在需要的情況下從遠程刷新圖片。
// 磁盤緩存將被NSURLCache處理而不是SDWebImage乙漓,因為SDWebImage會導致輕微的性能下載级历。
// 該標記幫助處理在相同請求URL后面改變的圖片。如果緩存圖片被刷新叭披,則完成block會使用緩存圖片調用一次
// 然后再用最終圖片調用一次
SDWebImageRefreshCached = 1 << 4,
// 在iOS 4+系統(tǒng)中寥殖,當程序進入后臺后繼續(xù)下載圖片。這將要求系統(tǒng)給予額外的時間讓請求完成
// 如果后臺任務超時涩蜘,則操作被取消
SDWebImageContinueInBackground = 1 << 5,
// 通過設置NSMutableURLRequest.HTTPShouldHandleCookies = YES;來處理存儲在NSHTTPCookieStore中的cookie
SDWebImageHandleCookies = 1 << 6,
// 允許不受信任的SSL認證
SDWebImageAllowInvalidSSLCertificates = 1 << 7,
// 默認情況下嚼贡,圖片下載按入隊的順序來執(zhí)行。該標記將其移到隊列的前面同诫,
// 以便圖片能立即下載而不是等到當前隊列被加載
SDWebImageHighPriority = 1 << 8,
// 默認情況下粤策,占位圖片在加載圖片的同時被加載。該標記延遲占位圖片的加載直到圖片已以被加載完成
SDWebImageDelayPlaceholder = 1 << 9,
// 通常我們不調用動畫圖片的transformDownloadedImage代理方法误窖,因為大多數轉換代碼可以管理它叮盘。
// 使用這個則不任何情況下都進行轉換。
SDWebImageTransformAnimatedImage = 1 << 10,
};
在-downloadImageWithURL:options:progress:completed:中霹俺,可以看到SDWebImageOptions與SDWebImageDownloaderOptions中的選項是如何對應起來的
總結:SDWebImageManager的幾個方法
?- (void)cancelAll? : 取消runningOperations中所有的操作柔吼,并全部刪除
??- (BOOL)isRunning? :檢查是否有操作在運行,這里的操作指的是下載和緩存組成的組合操作
? - downloadImageWithURL:options:progress:completed:? 核心方法
? - (BOOL)diskImageExistsForURL:(NSURL *)url? :指定url的圖片是否進行了磁盤緩存
視圖擴展
我在使用SDWebImage的時候丙唧,使用得最多的是UIImageView+WebCache中的針對UIImageView的擴展方法愈魏,這些擴展方法將UIImageView與WebCache集成在一起,來讓UIImageView對象擁有異步下載和緩存遠程圖片的能力想际。其中最核心的方法是-sd_setImageWithURL:placeholderImage:options:progress:completed:培漏,其使用SDWebImageManager單例對象下載并緩存圖片,完成后將圖片賦值給UIImageView對象的image屬性胡本,以使圖片顯示出來牌柄,其具體實現如下:
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {? ?
?...? ? if (url) {? ??
?__weak UIImageView *wself = self;? ??
?// 使用SDWebImageManager單例對象來下載圖片? ??
?idoperation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if (!wself) return;
dispatch_main_sync_safe(^{
if (!wself) return;
// 圖片下載完后顯示圖片
if (image) {
wself.image = image;
[wself setNeedsLayout];
} else {
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;
[wself setNeedsLayout];
}
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
} else {
...
}
}
除了擴展UIImageView之外,SDWebImage還擴展了UIView打瘪、UIButton友鼻、MKAnnotationView等視圖類,大家可以參考源碼闺骚。當然彩扔,如果不想使用這些擴展,則可以直接使用SDWebImageManager來下載圖片僻爽,這也是很OK的虫碉。
技術點
SDWebImage的主要任務就是圖片的下載和緩存。為了支持這些操作胸梆,它主要使用了以下知識點:
dispatch_barrier_sync函數:該方法用于對操作設置順序敦捧,確保在執(zhí)行完任務后才會執(zhí)行后續(xù)操作须板。該方法常用于確保類的線程安全性操作。
NSMutableURLRequest:用于創(chuàng)建一個網絡請求對象兢卵,我們可以根據需要來配置請求報頭等信息习瑰。
NSOperation及NSOperationQueue:操作隊列是Objective-C中一種高級的并發(fā)處理方法,現在它是基于GCD來實現的秽荤。相對于GCD來說甜奄,操作隊列的優(yōu)點是可以取消在任務處理隊列中的任務,另外在管理操作間的依賴關系方面也容易一些窃款。對SDWebImage中我們就看到了如何使用依賴將下載順序設置成后進先出的順序课兄。
NSURLConnection(7.0后NSURLSession):用于網絡請求及響應處理。在iOS7.0后晨继,蘋果推出了一套新的網絡請求接口烟阐,即NSURLSession類。
開啟一個后臺任務紊扬。
NSCache類:一個類似于集合的容器蜒茄。它存儲key-value對,這一點類似于NSDictionary類餐屎。我們通常用使用緩存來臨時存儲短時間使用但創(chuàng)建昂貴的對象扩淀。重用這些對象可以優(yōu)化性能,因為它們的值不需要重新計算啤挎。另外一方面,這些對象對于程序來說不是緊要的卵凑,在內存緊張時會被丟棄庆聘。
清理緩存圖片的策略:特別是最大緩存空間大小的設置。如果所有緩存文件的總大小超過這一大小勺卢,則會按照文件最后修改時間的逆序伙判,以每次一半的遞歸來移除那些過早的文件,直到緩存的實際大小小于我們設置的最大使用空間黑忱。
對圖片的解壓縮操作:這一操作可以查看SDWebImageDecoder.m中+decodedImageWithImage方法的實現宴抚。
對GIF圖片的處理
對WebP圖片的處理