圖片下載的這些回調(diào)信息存儲在SDWebImageDownloader類的URLOperations屬性中,該屬性是一個(gè)字典,key是圖片的URL地址,value則是一個(gè)SDWebImageDownloaderOperation對象,包含每個(gè)圖片的多組回調(diào)信息。由于我們允許多個(gè)圖片同時(shí)下載,因此可能會有多個(gè)線程同時(shí)操作URLOperations屬性。為了保證URLOperations操作(添加、刪除)的線程安全性,SDWebImageDownloader將這些操作作為一個(gè)個(gè)任務(wù)放到barrierQueue隊(duì)列中,并設(shè)置屏障來確保同一時(shí)間只有一個(gè)線程操作URLOperations屬性,我們以添加操作為例,如下代碼所示:
- (nullableSDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullableNSURL*)url
createCallback:(SDWebImageDownloaderOperation *(^)())createCallback {
...
// 1. 以dispatch_barrier_sync操作來保證同一時(shí)間只有一個(gè)線程能對URLOperations進(jìn)行操作
dispatch_barrier_sync(self.barrierQueue, ^{
SDWebImageDownloaderOperation *operation =self.URLOperations[url];
if(!operation) {
//2. 處理第一次URL的下載
operation = createCallback();
self.URLOperations[url] = operation;
__weakSDWebImageDownloaderOperation *woperation = operation;
operation.completionBlock = ^{
SDWebImageDownloaderOperation *soperation = woperation;
if(!soperation)return;
if(self.URLOperations[url] == soperation) {
[self.URLOperations removeObjectForKey:url];
};
};
}
// 3. 處理同一URL的同步下載請求的單個(gè)下載
iddownloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
token = [SDWebImageDownloadToken new];
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
});
returntoken;
}
整個(gè)下載管理器對于下載請求的管理都是放在downloadImageWithURL:options:progress:completed:方法里面來處理的,該方法調(diào)用了上面所提到的addProgressCallback:andCompletedBlock:forURL:createCallback:方法來將請求的信息存入管理器中,同時(shí)在創(chuàng)建回調(diào)的block中創(chuàng)建新的操作,配置之后將其放入downloadQueue操作隊(duì)列中,最后方法返回新創(chuàng)建的操作。其具體實(shí)現(xiàn)如下:
- (nullableSDWebImageDownloadToken *)downloadImageWithURL:(nullableNSURL*)url
options:(SDWebImageDownloaderOptions)options
progress:(nullableSDWebImageDownloaderProgressBlock)progressBlock
completed:(nullableSDWebImageDownloaderCompletedBlock)completedBlock {
__weakSDWebImageDownloader *wself =self;
return[selfaddProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
__strong__typeof(wself) sself = wself;
//超時(shí)時(shí)間
NSTimeIntervaltimeoutInterval = sself.downloadTimeout;
if(timeoutInterval ==0.0) {
timeoutInterval =15.0;
}
// 1. 創(chuàng)建請求對象,并根據(jù)options參數(shù)設(shè)置其屬性
// 為了避免潛在的重復(fù)緩存(NSURLCache + SDImageCache),如果沒有明確告知需要緩存,則禁用圖片請求的緩存操作
NSMutableURLRequest*request = [[NSMutableURLRequestalloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ?NSURLRequestUseProtocolCachePolicy:NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining =YES;
if(sself.headersFilter) {
request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaderscopy]);
}
else{
request.allHTTPHeaderFields = sself.HTTPHeaders;
}
SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
operation.shouldDecompressImages = sself.shouldDecompressImages;
if(sself.urlCredential) {
operation.credential = sself.urlCredential;
}elseif(sself.username && sself.password) {
operation.credential = [NSURLCredentialcredentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
}
if(options & SDWebImageDownloaderHighPriority) {
operation.queuePriority =NSOperationQueuePriorityHigh;
}elseif(options & SDWebImageDownloaderLowPriority) {
operation.queuePriority =NSOperationQueuePriorityLow;
}
// 2. 將操作加入到操作隊(duì)列downloadQueue中
// 如果是LIFO順序,則將新的操作作為原隊(duì)列中最后一個(gè)操作的依賴,然后將新操作設(shè)置為最后一個(gè)操作
[sself.downloadQueue addOperation:operation];
if(sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
returnoperation;
}];
}
另外,每個(gè)下載操作的超時(shí)時(shí)間可以通過downloadTimeout屬性來設(shè)置,默認(rèn)值為15秒。
每個(gè)圖片的下載操作都是一個(gè)Operation操作。。我們在上面分析過這個(gè)操作的創(chuàng)建及加入操作隊(duì)列的過程?,F(xiàn)在我們來看看單個(gè)操作的具體實(shí)現(xiàn)。
SDWebImage定義了一個(gè)協(xié)議,即SDWebImageOperation作為圖片下載操作的基礎(chǔ)協(xié)議。它只聲明了一個(gè)cancel方法,用于取消操作。協(xié)議的具體聲明如下:
@protocolSDWebImageOperation
- (void)cancel;
@end
SDWebImage還定義了一個(gè)下載協(xié)議,即SDWebImageDownloaderOperationInterface,它允許用戶自定義下載操作,當(dāng)然,SDWebImage也提供了自己的下載類,即SDWebImageDownloaderOperation,它繼承自NSOperation,并采用了SDWebImageOperation和SDWebImageDownloaderOperationInterface協(xié)議。并且實(shí)現(xiàn)他們的代理方法。
對于圖片的下載,SDWebImageDownloaderOperation完全依賴于URL加載系統(tǒng)中的NSURLSession類。我們先來分析一下SDWebImageDownloaderOperation類中對于圖片實(shí)際數(shù)據(jù)的下載處理,即NSURLSessionDataDelegate和NSURLSessionDataDelegate各個(gè)代理方法的實(shí)現(xiàn)。(ps 有關(guān)NSURLSession類的具體介紹請戳這里)
我們前面說過SDWebImageDownloaderOperation類是繼承自NSOperation類。它沒有簡單的實(shí)現(xiàn)main方法,而是采用更加靈活的start方法,以便自己管理下載的狀態(tài)。
在start方法中,創(chuàng)建了我們下載所使用的NSURLSession對象,開啟了圖片的下載,同時(shí)拋出一個(gè)下載開始的通知。當(dāng)然,如果我們期望下載在后臺處理,則只需要配置我們的下載選項(xiàng),使其包含SDWebImageDownloaderContinueInBackground選項(xiàng)。start方法的具體實(shí)現(xiàn)如下:
- (void)start {
@synchronized(self) {
// 管理下載狀態(tài),如果已取消,則重置當(dāng)前下載并設(shè)置完成狀態(tài)為YES
if(self.isCancelled) {
self.finished =YES;
[selfreset];
return;
}
...
NSURLSession*session =self.unownedSession;
if(!self.unownedSession) {
//如果session為空,創(chuàng)建session
NSURLSessionConfiguration*sessionConfig = [NSURLSessionConfigurationdefaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest =15;
self.ownedSession = [NSURLSessionsessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
session =self.ownedSession;
}
//創(chuàng)建下載任務(wù)
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing =YES;
}
//開啟下載任務(wù)
[self.dataTask resume];
if(self.dataTask) {
for(SDWebImageDownloaderProgressBlock progressBlockin[selfcallbacksForKey:kProgressCallbackKey]) {
progressBlock(0,NSURLResponseUnknownLength,self.request.URL);
}
// 2. 在主線程拋出下載開始通知
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenterdefaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
}else{
[selfcallCompletionBlocksWithError:[NSErrorerrorWithDomain:NSURLErrorDomaincode:0userInfo:@{NSLocalizedDescriptionKey:@"Connection can't be initialized"}]];
}
...
}
我們先看看NSURLSessionDataDelegate代理的具體實(shí)現(xiàn):
- (void)URLSession:(NSURLSession*)session
dataTask:(NSURLSessionDataTask*)dataTask
didReceiveResponse:(NSURLResponse*)response
completionHandler:(void(^)(NSURLSessionResponseDispositiondisposition))completionHandler {
//接收到服務(wù)器響應(yīng)
if(![response respondsToSelector:@selector(statusCode)] || (((NSHTTPURLResponse*)response).statusCode <400&& ((NSHTTPURLResponse*)response).statusCode !=304)) {
//如果服務(wù)器狀態(tài)碼正常,并且不是304,(因?yàn)?04表示遠(yuǎn)程圖片并沒有改變,當(dāng)前緩存的圖片就可以使用)拿到圖片的大小。并進(jìn)度回調(diào)
NSIntegerexpected = response.expectedContentLength >0? (NSInteger)response.expectedContentLength :0;
self.expectedSize = expected;
for(SDWebImageDownloaderProgressBlock progressBlockin[selfcallbacksForKey:kProgressCallbackKey]) {
progressBlock(0, expected,self.request.URL);
}
//根據(jù)返回?cái)?shù)據(jù)大小創(chuàng)建一個(gè)數(shù)據(jù)Data容器
self.imageData = [[NSMutableDataalloc] initWithCapacity:expected];
self.response = response;
dispatch_async(dispatch_get_main_queue(), ^{
//發(fā)送接收到服務(wù)器響應(yīng)通知
[[NSNotificationCenterdefaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self];
});
}
else{
//狀態(tài)碼錯(cuò)誤
NSUIntegercode = ((NSHTTPURLResponse*)response).statusCode;
//判斷是不是304
if(code ==304) {
[selfcancelInternal];
}else{
[self.dataTask cancel];
}
dispatch_async(dispatch_get_main_queue(), ^{
//發(fā)出停止下載通知
[[NSNotificationCenterdefaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
});
//錯(cuò)誤回調(diào)
[selfcallCompletionBlocksWithError:[NSErrorerrorWithDomain:NSURLErrorDomaincode:((NSHTTPURLResponse*)response).statusCode userInfo:nil]];
//重置
[selfdone];
}
if(completionHandler) {
completionHandler(NSURLSessionResponseAllow);
}
}
- (void)URLSession:(NSURLSession*)session dataTask:(NSURLSessionDataTask*)dataTask didReceiveData:(NSData*)data {
//1. 接收服務(wù)器返回?cái)?shù)據(jù) 往容器中追加數(shù)據(jù)
[self.imageData appendData:data];
if((self.options & SDWebImageDownloaderProgressiveDownload) &&self.expectedSize >0) {
//2. 獲取已下載數(shù)據(jù)總大小
constNSIntegertotalSize =self.imageData.length;
// 3. 更新數(shù)據(jù)源,我們需要傳入所有數(shù)據(jù),而不僅僅是新數(shù)據(jù)
CGImageSourceRefimageSource =CGImageSourceCreateWithData((__bridgeCFDataRef)self.imageData,NULL);
// 4. 首次獲取到數(shù)據(jù)時(shí),從這些數(shù)據(jù)中獲取圖片的長、寬、方向?qū)傩灾?/p>
if(width + height ==0) {
CFDictionaryRefproperties =CGImageSourceCopyPropertiesAtIndex(imageSource,0,NULL);
if(properties) {
NSIntegerorientationValue =-1;
CFTypeRefval =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);
// 5. 當(dāng)繪制到Core Graphics時(shí),我們會丟失方向信息,這意味著有時(shí)候由initWithCGIImage創(chuàng)建的圖片
//? ? 的方向會不對,所以在這邊我們先保存這個(gè)信息并在后面使用。
#if SD_UIKIT || SD_WATCH
orientation = [[selfclass] orientationFromPropertyValue:(orientationValue ==-1?1: orientationValue)];
#endif
}
}
// 6. 圖片還未下載完成
if(width + height >0&& totalSize
// 7. 使用現(xiàn)有的數(shù)據(jù)創(chuàng)建圖片對象,如果數(shù)據(jù)中存有多張圖片,則取第一張
CGImageRefpartialImageRef =CGImageSourceCreateImageAtIndex(imageSource,0,NULL);
#if SD_UIKIT || SD_WATCH
// 8. 適用于iOS變形圖像的解決方案。我的理解是由于iOS只支持RGB顏色空間,所以在此對下載下來的圖片做個(gè)顏色空間轉(zhuǎn)換處理。
if(partialImageRef) {
constsize_t partialHeight =CGImageGetHeight(partialImageRef);
CGColorSpaceRefcolorSpace =CGColorSpaceCreateDeviceRGB();
CGContextRefbmContext =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. 對圖片進(jìn)行縮放、解碼操作
if(partialImageRef) {
#if SD_UIKIT || SD_WATCH
UIImage*image = [UIImageimageWithCGImage:partialImageRef scale:1orientation:orientation];
#elif SD_MAC
UIImage*image = [[UIImagealloc] initWithCGImage:partialImageRef size:NSZeroSize];
#endif
NSString*key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
UIImage*scaledImage = [selfscaledImageForKey:key image:image];
if(self.shouldDecompressImages) {
image = [UIImagedecodedImageWithImage:scaledImage];
}
else{
image = scaledImage;
}
CGImageRelease(partialImageRef);
[selfcallCompletionBlocksWithImage:image imageData:nilerror:nilfinished:NO];
}
}
CFRelease(imageSource);
}
for(SDWebImageDownloaderProgressBlock progressBlockin[selfcallbacksForKey:kProgressCallbackKey]) {
progressBlock(self.imageData.length,self.expectedSize,self.request.URL);
}
}
當(dāng)然,在下載完成或下載失敗后,會調(diào)用NSURLSessionTaskDelegate的- (void)URLSession: task: didCompleteWithError:代理方法,并清除連接,并拋出下載停止的通知。如果下載成功,則會處理完整的圖片數(shù)據(jù),對其進(jìn)行適當(dāng)?shù)目s放與解壓縮操作,以提供給完成回調(diào)使用。
下載的核心其實(shí)就是利用NSURLSession對象來加載數(shù)據(jù)。每個(gè)圖片的下載都由一個(gè)Operation操作來完成,并將這些操作放到一個(gè)操作隊(duì)列中。這樣可以實(shí)現(xiàn)圖片的并發(fā)下載。
為了減少網(wǎng)絡(luò)流量的消耗,我們都希望下載下來的圖片緩存到本地,下次再去獲取同一張圖片時(shí),可以直接從本地獲取,而不再從遠(yuǎn)程服務(wù)器獲取。這樣做的一個(gè)好處是提升了用戶體驗(yàn),用戶第二次查看同一幅圖片時(shí),能快速從本地獲取圖片直接呈現(xiàn)給用戶。
SDWebImage提供了對圖片緩存的支持,而該功能是由SDImageCache類完成的。該類負(fù)責(zé)處理內(nèi)存緩存及一個(gè)可選的磁盤緩存。其中磁盤緩存的寫操作是異步的,這樣就不會對UI操作造成影響。
另外說明,在4.0以后新添加一個(gè)緩存配置類SDImageCacheConfig,主要是一些緩存策略的配置。其頭文件定義如下:
/**
是否在緩存的時(shí)候解壓縮,默認(rèn)是YES 可以提高性能 但是會耗內(nèi)存。 當(dāng)使用SDWebImage 因?yàn)閮?nèi)存而崩潰 可以將其設(shè)置為NO
*/
@property(assign,nonatomic)BOOLshouldDecompressImages;
/**
* 是否禁用 iCloud 備份 默認(rèn)YES
*/
@property(assign,nonatomic)BOOLshouldDisableiCloud;
/**
* 內(nèi)存緩存? 默認(rèn)YES
*/
@property(assign,nonatomic)BOOLshouldCacheImagesInMemory;
/**
* 最大磁盤緩存時(shí)間 默認(rèn)一周 單位秒
*/
@property(assign,nonatomic)NSIntegermaxCacheAge;
/**
* 最大緩存容量 0 表示無限緩存? 單位字節(jié)
*/
@property(assign,nonatomic)NSUIntegermaxCacheSize;
內(nèi)存緩存的處理使用NSCache對象來實(shí)現(xiàn)的。NSCache是一個(gè)類似與集合的容器。它存儲key-value對,這一點(diǎn)類似于NSDictionary類。我們通常使用緩存來臨時(shí)存儲短時(shí)間使用但創(chuàng)建昂貴的對象。重用這些對象可以優(yōu)化性能,因?yàn)樗鼈兊闹挡恍枰匦掠?jì)算。另外一方面,這些對象對于程序員來說不是緊要的,在內(nèi)存緊張時(shí)會被丟棄。
磁盤緩存的處理則是使用NSFileManager對象來實(shí)現(xiàn)的。圖片存儲的位置是位于Caches文件夾中的default文件夾下。另外,SDImageCache還定義了一個(gè)串行隊(duì)列,來異步存儲圖片。
內(nèi)存緩存與磁盤緩存相關(guān)變量的聲明及定義如下:
@interfaceSDImageCache()
#pragma mark - Properties
@property(strong,nonatomic,nonnull)NSCache*memCache;
@property(strong,nonatomic,nonnull)NSString*diskCachePath;
@property(strong,nonatomic,nullable)NSMutableArray *customPaths;
@property(SDDispatchQueueSetterSementics,nonatomic,nullable)dispatch_queue_tioQueue;
@end
@implementationSDImageCache{
NSFileManager*_fileManager;
}
- (nonnullinstancetype)initWithNamespace:(nonnullNSString*)ns
diskCacheDirectory:(nonnullNSString*)directory {
if((self= [superinit])) {
NSString*fullNamespace = [@"com.hackemist.SDWebImageCache."stringByAppendingString:ns];
// 隊(duì)列
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
//緩存配置
_config = [[SDImageCacheConfig alloc] init];
// 內(nèi)存緩存
_memCache = [[AutoPurgeCache alloc] init];
_memCache.name = fullNamespace;
// 初始化磁盤緩存路徑
if(directory !=nil) {
_diskCachePath = [directory stringByAppendingPathComponent:fullNamespace];
}else{
NSString*path = [selfmakeDiskCachePath:ns];
_diskCachePath = path;
}
dispatch_sync(_ioQueue, ^{
_fileManager = [NSFileManagernew];
});
}
returnself;
}
@end
SDImageCache提供了大量方法來緩存、獲取、移除、及清空圖片。而對于每一個(gè)圖片,為了方便地在內(nèi)存或磁盤中對它進(jìn)行這些操作,我們需要一個(gè)key值來索引它。在內(nèi)存中,我們將其作為NSCache的key值,而在磁盤中,我們用作這個(gè)key作為圖片的文件名。對于一個(gè)遠(yuǎn)程服務(wù)器下載的圖片,其url實(shí)作為這個(gè)key的最佳選擇了。我們在后面會看到這個(gè)key值得重要性。
我們先來看看圖片的緩存操作,該操作會在內(nèi)存中放置一份緩存,而如果確定需要緩存到磁盤,則將磁盤緩存操作作為一個(gè)task放到串行隊(duì)列中處理。在iOS中,會先檢測圖片是PNG還是JPEG,并將其轉(zhuǎn)換為相應(yīng)的圖片數(shù)據(jù),最后將數(shù)據(jù)寫入到磁盤中(文件名是對key值做MD5摘要后的串)。緩存操作的基礎(chǔ)方法是:-storeImage:imageData:forKey:toDisk:completion:,它的具體實(shí)現(xiàn)如下:
- (void)storeImage:(nullableUIImage*)image
imageData:(nullableNSData*)imageData
forKey:(nullableNSString*)key
toDisk:(BOOL)toDisk
completion:(nullableSDWebImageNoParamsBlock)completionBlock {
if(!image || !key) {
if(completionBlock) {
completionBlock();
}
return;
}
// 內(nèi)存緩存 將其存入NSCache中,同時(shí)傳入圖片的消耗值
if(self.config.shouldCacheImagesInMemory) {
NSUIntegercost = SDCacheCostForImage(image);
[self.memCache setObject:image forKey:key cost:cost];
}
// 如果確定需要磁盤緩存,則將緩存操作作為一個(gè)任務(wù)放入ioQueue中
if(toDisk) {
dispatch_async(self.ioQueue, ^{
NSData*data = imageData;
if(!data && image) {
//如果imageData為nil 需要確定圖片是PNG還是JPEG。PNG圖片容易檢測,因?yàn)橛幸粋€(gè)唯一簽名。PNG圖像的前8個(gè)字節(jié)總是包含以下值:137 80 78 71 13 10 26 10
//判斷 圖片是何種類型 使用 sd_imageFormatForImageData 來判斷
// SDImageFormat 是一個(gè)枚舉? 其定義如下:
//? ? ? ? ? ? ? ? typedef NS_ENUM(NSInteger, SDImageFormat) {
//? ? ? ? ? ? ? ? ? ? SDImageFormatUndefined = -1,
//? ? ? ? ? ? ? ? ? ? SDImageFormatJPEG = 0,
//? ? ? ? ? ? ? ? ? ? SDImageFormatPNG,
//? ? ? ? ? ? ? ? ? ? SDImageFormatGIF,
//? ? ? ? ? ? ? ? ? ? SDImageFormatTIFF,
//? ? ? ? ? ? ? ? ? ? SDImageFormatWebP
//? ? ? ? ? ? ? ? };
SDImageFormat imageFormatFromData = [NSDatasd_imageFormatForImageData:data];
//根據(jù)圖片類型 轉(zhuǎn)成data
data = [image sd_imageDataAsFormat:imageFormatFromData];
}
// 4. 創(chuàng)建緩存文件并存儲圖片
[selfstoreImageDataToDisk:data forKey:key];
if(completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}else{
if(completionBlock) {
completionBlock();
}
}
}
如果我們想在內(nèi)存或磁盤中查詢是否有key指定的圖片,則可以分別使用以下方法:
//快速查詢圖片是否已經(jīng)磁盤緩存 不返回圖片 只做快速查詢 異步操作
- (void)diskImageExistsWithKey:(nullableNSString*)key completion:(nullableSDWebImageCheckCacheCompletionBlock)completionBlock;
//異步查詢圖片 不管是內(nèi)存緩存還是磁盤緩存
- (nullableNSOperation*)queryCacheOperationForKey:(nullableNSString*)key done:(nullableSDCacheQueryCompletedBlock)doneBlock;
//從內(nèi)存中查詢圖片
- (nullableUIImage*)imageFromMemoryCacheForKey:(nullableNSString*)key;
//從磁盤中查詢圖片
- (nullableUIImage*)imageFromDiskCacheForKey:(nullableNSString*)key;
//同步查詢圖片,不管是內(nèi)存緩存還是磁盤緩存
- (nullableUIImage*)imageFromCacheForKey:(nullableNSString*)key;
其實(shí)- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key內(nèi)部實(shí)現(xiàn)是調(diào)用了- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key和- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key方法,如下:
- (nullableUIImage*)imageFromCacheForKey:(nullableNSString*)key {
// 從緩存中查找圖片
UIImage*image = [selfimageFromMemoryCacheForKey:key];
if(image) {
returnimage;
}
// 從磁盤中查找圖片
image = [selfimageFromDiskCacheForKey:key];
returnimage;
}
我們再來看看異步查詢圖片的具體實(shí)現(xiàn):
- (nullableNSOperation*)queryCacheOperationForKey:(nullableNSString*)key done:(nullableSDCacheQueryCompletedBlock)doneBlock {
...
// 1. 首先查看內(nèi)存緩存,如果查找到,則直接回調(diào)doneBlock并返回
UIImage*image = [selfimageFromMemoryCacheForKey:key];
if(image) {
NSData*diskData =nil;
//進(jìn)行了是否是GIF的判斷
if([image isGIF]) {
diskData = [selfdiskImageDataBySearchingAllPathsForKey:key];
}
if(doneBlock) {
doneBlock(image, diskData, SDImageCacheTypeMemory);
}
returnnil;
}
// 2. 如果內(nèi)存中沒有,則在磁盤中查找。如果找到,則將其放到內(nèi)存緩存,并調(diào)用doneBlock回調(diào)
NSOperation*operation = [NSOperationnew];
dispatch_async(self.ioQueue, ^{
if(operation.isCancelled) {
// do not call the completion if cancelled
return;
}
@autoreleasepool{
NSData*diskData = [selfdiskImageDataBySearchingAllPathsForKey:key];
UIImage*diskImage = [selfdiskImageForKey:key];
if(diskImage &&self.config.shouldCacheImagesInMemory) {
//進(jìn)行內(nèi)存緩存
NSUIntegercost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
if(doneBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
});
}
}
});
returnoperation;
}
圖片的移除操作則可以使用以下方法:
//從內(nèi)存和磁盤中移除圖片
- (void)removeImageForKey:(nullableNSString*)key withCompletion:(nullableSDWebImageNoParamsBlock)completion;
//從內(nèi)存 或 可選磁盤中移除圖片
- (void)removeImageForKey:(nullableNSString*)key fromDisk:(BOOL)fromDisk withCompletion:(nullableSDWebImageNoParamsBlock)completion;
我們可以選擇同時(shí)移除內(nèi)存及磁盤上的圖片,或者只移除內(nèi)存中的圖片。
磁盤緩存圖片的操作可以分為完全清空和部分清理。完全清空操作是直接把緩存的文件夾移除,部分清理是清理掉過時(shí)的舊圖片,清空操作有以下方法:
//清除內(nèi)存緩存
- (void)clearMemory;
//完全清空磁盤緩存
- (void)clearDiskOnCompletion:(nullableSDWebImageNoParamsBlock)completion;
//清空舊圖片
- (void)deleteOldFilesWithCompletionBlock:(nullableSDWebImageNoParamsBlock)completionBlock;
而部分清理則是根據(jù)我們設(shè)定的一些參數(shù)來移除一些文件,這里主要有兩個(gè)指標(biāo):文件的緩存有效期及最大緩存空間大小。文件的緩存有效期可以通過SDImageCacheConfig類的maxCacheAge屬性來設(shè)置,默認(rèn)是1周的時(shí)間。如果文件的緩存時(shí)間超過這個(gè)時(shí)間值,則將其移除。而最大緩存空間大小是通過maxCacheSize屬性來設(shè)置的,如果所有緩存文件的總大小超過這一大小,則會按照文件最后修改時(shí)間的逆序,以每次一半的遞歸來移除那些過早的文件,直到緩存的實(shí)際大小小于我們設(shè)置的最大使用空間。清理的操作在-deleteOldFilesWithCompletionBlock:方法中,其實(shí)現(xiàn)如下:
- (void)deleteOldFilesWithCompletionBlock:(nullableSDWebImageNoParamsBlock)completionBlock {
dispatch_async(self.ioQueue, ^{
NSURL*diskCacheURL = [NSURLfileURLWithPath:self.diskCachePath isDirectory:YES];
NSArray *resourceKeys = @[NSURLIsDirectoryKey,NSURLContentModificationDateKey,NSURLTotalFileAllocatedSizeKey];
// 1. 該枚舉器預(yù)先獲取緩存文件的有用的屬性
NSDirectoryEnumerator*fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
NSDate*expirationDate = [NSDatedateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
NSMutableDictionary *> *cacheFiles = [NSMutableDictionarydictionary];
NSUIntegercurrentCacheSize =0;
// 2. 枚舉緩存文件夾中所有文件,該迭代有兩個(gè)目的:移除比過期日期更老的文件;存儲文件屬性以備后面執(zhí)行基于緩存大小的清理操作
NSMutableArray *urlsToDelete = [[NSMutableArrayalloc] init];
for(NSURL*fileURLinfileEnumerator) {
NSError*error;
NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
// 3. 跳過文件夾
if(error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}
// 4. 移除早于有效期的老文件
NSDate*modificationDate = resourceValues[NSURLContentModificationDateKey];
if([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
// 5. 存儲文件的引用并計(jì)算所有文件的總大小,以備后用
NSNumber*totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
cacheFiles[fileURL] = resourceValues;
}
for(NSURL*fileURLinurlsToDelete) {
[_fileManager removeItemAtURL:fileURL error:nil];
}
//6.如果磁盤緩存的大小大于我們配置的最大大小,則執(zhí)行基于文件大小的清理,我們首先刪除最老的文件
if(self.config.maxCacheSize >0&& currentCacheSize >self.config.maxCacheSize) {
// 7. 以設(shè)置的最大緩存大小的一半作為清理目標(biāo)
constNSUIntegerdesiredCacheSize =self.config.maxCacheSize /2;
// 8. 按照最后修改時(shí)間來排序剩下的緩存文件
NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(idobj1,idobj2) {
return[obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];
// 9. 刪除文件,直到緩存總大小降到我們期望的大小
for(NSURL*fileURLinsortedFiles) {
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();
});
}
});
}
以上分析了圖片緩存操作,當(dāng)然,除了上面講的幾個(gè)操作,SDWebImage類還提供了一些輔助方法。如獲取緩存大小、緩存中圖片的數(shù)量、判斷緩存中是否存在某個(gè)key指定的圖片。另外,SDWebImage類提供了一個(gè)單例方法的實(shí)現(xiàn),所以我們可以將其當(dāng)做單例對象來處理。
在實(shí)際的運(yùn)用中,我們并不直接使用SDWebImageDownloader類及SDImageCache類來執(zhí)行圖片的下載及緩存。為了方便用戶的使用,SDWebImage提供了SDWebImageManager對象來管理圖片的下載與緩存。而我們經(jīng)常用到的諸如UIImageView+WebCache等控件的分類都是基于SDWebImageManager對象的,該對象將一個(gè)下載器和一個(gè)圖片緩存綁定在一起,并對外提供兩個(gè)只讀屬性來獲取它們,如下代碼所示:
@interfaceSDWebImageManager:NSObject
@property(weak,nonatomic)id delegate;
@property(strong,nonatomic,readonly) SDImageCache *imageCache;
@property(strong,nonatomic,readonly) SDWebImageDownloader *imageDownloader;
...
@end
從上面的代碼中我們還可以看到一個(gè)delegate屬性,它是一個(gè)id 對象。SDWebImageManagerDelegate聲明了兩個(gè)可選實(shí)現(xiàn)的方法,如下所示:
// 控制當(dāng)圖片在緩存中沒有找到時(shí),應(yīng)該下載哪個(gè)圖片
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL*)imageURL;
// 允許在圖片已經(jīng)被下載完成且被緩存到磁盤或內(nèi)存前立即轉(zhuǎn)換
- (UIImage*)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage*)image withURL:(NSURL*)imageURL;
這兩個(gè)代理方法會在SDWebImageManager的-downloadImageWithURL:options:progress:completed:方法中調(diào)用,而這個(gè)方法是SDWebImageManager類的核心所在。我們來看看它具體的實(shí)現(xiàn):
- (id)loadImageWithURL:(nullableNSURL*)url
options:(SDWebImageOptions)options
progress:(nullableSDWebImageDownloaderProgressBlock)progressBlock
completed:(nullableSDInternalCompletionBlock)completedBlock {
...
// 前面省略n行。主要作了如下處理:
// 1. 判斷url的合法性
// 2. 創(chuàng)建SDWebImageCombinedOperation對象
// 3. 查看url是否是之前下載失敗過的
// 4. 如果url為nil,或者在不可重試的情況下是一個(gè)下載失敗過的url,則直接返回操作對象并調(diào)用完成回調(diào)
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage*cachedImage,NSData*cachedData, SDImageCacheType cacheType) {
if(operation.isCancelled) {
[selfsafelyRemoveOperationFromRunning:operation];
return;
}
//先去緩存中查找圖片,如果圖片不存在? 或者 當(dāng)前圖片的下載模式是 SDWebImageRefreshCached 開始下載
if((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:selfshouldDownloadImageForURL:url])) {
...
//下載
SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage*downloadedImage,NSData*downloadedData,NSError*error,BOOLfinished) {
__strong__typeof(weakOperation) strongOperation = weakOperation;
if(!strongOperation || strongOperation.isCancelled) {
// 操作被取消,則不做任務(wù)事情
}elseif(error) {
// 如果出錯(cuò),則調(diào)用完成回調(diào),并將url放入下載失敗url數(shù)組中
...
}
else{
...
BOOLcacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
if(options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
// Image refresh hit the NSURLCache cache, do not call the completion block
}elseif(downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
// 在全局隊(duì)列中并行處理圖片的緩存
// 首先對圖片做個(gè)轉(zhuǎn)換操作,該操作是代理對象實(shí)現(xiàn)的
// 然后對圖片做緩存處理
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0), ^{
UIImage*transformedImage = [self.delegate imageManager:selftransformDownloadedImage:downloadedImage withURL:url];
if(transformedImage && finished) {
BOOLimageWasTransformed = ![transformedImage isEqual:downloadedImage];
// pass nil if the image was transformed, so we can recalculate the data from the image
[self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ?nil: downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
}
...
});
}else{
if(downloadedImage && finished) {
[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
}
...
}
}
// 下載完成并緩存后,將操作從隊(duì)列中移除
if(finished) {
[selfsafelyRemoveOperationFromRunning:strongOperation];
}
}];
operation.cancelBlock = ^{
[self.imageDownloader cancel:subOperationToken];
__strong__typeof(weakOperation) strongOperation = weakOperation;
[selfsafelyRemoveOperationFromRunning:strongOperation];
};
}elseif(cachedImage) {
...
}else{
...
}
}];
returnoperation;
}
對于這個(gè)方法,我們沒有做過多的解釋。其主要就是下載圖片并根據(jù)操作選項(xiàng)緩存圖片。上面這個(gè)下載方法的操作選項(xiàng)參數(shù)是由枚舉SDWebImageOptions來定義的,這個(gè)操作中的一些選項(xiàng)是與SDWebImageDownloaderOptions中的選項(xiàng)對應(yīng)的,我們來看看這個(gè)SDWebImageOptions選項(xiàng)都有哪些:
typedefNS_OPTIONS(NSUInteger, SDWebImageOptions) {
// 默認(rèn)情況下,當(dāng)URL下載失敗時(shí),URL會被列入黑名單,導(dǎo)致庫不會再去重試,該標(biāo)記用于禁用黑名單
SDWebImageRetryFailed =1<<0,
// 默認(rèn)情況下,圖片下載開始于UI交互,該標(biāo)記禁用這一特性,這樣下載延遲到UIScrollView減速時(shí)
SDWebImageLowPriority =1<<1,
// 該標(biāo)記禁用磁盤緩存
SDWebImageCacheMemoryOnly =1<<2,
// 該標(biāo)記啟用漸進(jìn)式下載,圖片在下載過程中是漸漸顯示的,如同瀏覽器一下。
// 默認(rèn)情況下,圖像在下載完成后一次性顯示
SDWebImageProgressiveDownload =1<<3,
// 即使圖片緩存了,也期望HTTP響應(yīng)cache control,并在需要的情況下從遠(yuǎn)程刷新圖片。
// 磁盤緩存將被NSURLCache處理而不是SDWebImage,因?yàn)镾DWebImage會導(dǎo)致輕微的性能下載。
// 該標(biāo)記幫助處理在相同請求URL后面改變的圖片。如果緩存圖片被刷新,則完成block會使用緩存圖片調(diào)用一次
// 然后再用最終圖片調(diào)用一次
SDWebImageRefreshCached =1<<4,
// 在iOS 4+系統(tǒng)中,當(dāng)程序進(jìn)入后臺后繼續(xù)下載圖片。這將要求系統(tǒng)給予額外的時(shí)間讓請求完成
// 如果后臺任務(wù)超時(shí),則操作被取消
SDWebImageContinueInBackground =1<<5,
// 通過設(shè)置NSMutableURLRequest.HTTPShouldHandleCookies = YES;來處理存儲在NSHTTPCookieStore中的cookie
SDWebImageHandleCookies =1<<6,
// 允許不受信任的SSL認(rèn)證
SDWebImageAllowInvalidSSLCertificates =1<<7,
// 默認(rèn)情況下,圖片下載按入隊(duì)的順序來執(zhí)行。該標(biāo)記將其移到隊(duì)列的前面,
// 以便圖片能立即下載而不是等到當(dāng)前隊(duì)列被加載
SDWebImageHighPriority =1<<8,
// 默認(rèn)情況下,占位圖片在加載圖片的同時(shí)被加載。該標(biāo)記延遲占位圖片的加載直到圖片已以被加載完成
SDWebImageDelayPlaceholder =1<<9,
// 通常我們不調(diào)用動畫圖片的transformDownloadedImage代理方法,因?yàn)榇蠖鄶?shù)轉(zhuǎn)換代碼可以管理它。
// 使用這個(gè)票房則不任何情況下都進(jìn)行轉(zhuǎn)換。
SDWebImageTransformAnimatedImage =1<<10,
};
大家再看-downloadImageWithURL:options:progress:completed:,可以看到兩個(gè)SDWebImageOptions與SDWebImageDownloaderOptions中的選項(xiàng)是如何對應(yīng)起來的,在此不多做解釋。
我們在使用SDWebImage的時(shí)候,使用最多的是UIImageView+WebCache中的針對UIImageView的擴(kuò)展方法,這些擴(kuò)展方法將UIImageView與WebCache集成在一起,來讓UIImageView對象擁有異步下載和緩存遠(yuǎn)程圖片的能力。在4.0.0版本以后,給UIView新增了好多方法,其中最之前UIImageView+WebCache最核心的方法-sd_setImageWithURL:placeholderImage:options:progress:completed:,現(xiàn)在使用的是UIView+WebCache中新增的方法sd_internalSetImageWithURL:placeholderImage:options:operationKey:setImageBlock:progress:completed:,其使用SDWebImageManager單例對象下載并緩存圖片,完成后將圖片賦值給UIImageView對象的image屬性,以使圖片顯示出來,其具體實(shí)現(xiàn)如下:
- (void)sd_internalSetImageWithURL:(nullableNSURL*)url
placeholderImage:(nullableUIImage*)placeholder
options:(SDWebImageOptions)options
operationKey:(nullableNSString*)operationKey
setImageBlock:(nullableSDSetImageBlock)setImageBlock
progress:(nullableSDWebImageDownloaderProgressBlock)progressBlock
completed:(nullableSDExternalCompletionBlock)completedBlock {
...
if(url) {
// check if activityView is enabled or not
if([selfsd_showActivityIndicatorView]) {
[selfsd_addActivityIndicator];
}
__weak__typeof(self)wself =self;
// 使用SDWebImageManager單例對象來下載圖片
id operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage*image,NSData*data,NSError*error, SDImageCacheType cacheType,BOOLfinished,NSURL*imageURL) {
__strong__typeof(wself) sself = wself;
[sself sd_removeActivityIndicator];
if(!sself) {
return;
}
dispatch_main_async_safe(^{
if(!sself) {
return;
}
if(image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
completedBlock(image, error, cacheType, url);
return;
}elseif(image) {
// 圖片下載完后顯示圖片
[sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];
}else{
if((options & SDWebImageDelayPlaceholder)) {
[sself sd_setImage:placeholder imageData:nilbasedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];
}
}
if(completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
[selfsd_setImageLoadOperation:operation forKey:validOperationKey];
}else{
...
}
}
除了擴(kuò)展UIImageView之外,SDWebImage還擴(kuò)展了UIView、UIButton、MKAnnotationView等視圖類,大家可以參考源碼。
當(dāng)然,如果不想使用這些擴(kuò)展,則可以直接使用SDWebImageManager來下載圖片,這也是很OK的。
SDWebImage的主要任務(wù)就是圖片的下載和緩存。為了支持這些操作,它主要使用了以下知識點(diǎn):
dispatch_barrier_sync函數(shù):該方法用于對操作設(shè)置等待,確保在執(zhí)行完任務(wù)后才會執(zhí)行后續(xù)操作。該方法常用于確保類的線程安全性操作。
NSMutableURLRequest:用于創(chuàng)建一個(gè)網(wǎng)絡(luò)請求對象,我們可以根據(jù)需要來配置請求報(bào)頭等信息。
NSOperation及NSOperationQueue:操作隊(duì)列是Objective-C中一種高級的并發(fā)處理方法,現(xiàn)在它是基于GCD來實(shí)現(xiàn)的。相對于GCD來說,操作隊(duì)列的優(yōu)點(diǎn)是可以取消在任務(wù)處理隊(duì)列中的任務(wù),另外在管理操作間的依賴關(guān)系方面也容易一些。對SDWebImage中我們就看到了如何使用依賴將下載順序設(shè)置成后進(jìn)先出的順序。(有興趣的同學(xué)可以看看我這篇博客->聊一聊NSOperation的那些事)
NSURLSession:用于網(wǎng)絡(luò)請求及響應(yīng)處理。在iOS7.0后,蘋果推出了一套新的網(wǎng)絡(luò)請求接口,即NSURLSession類。(有興趣的同學(xué)可以看看我這篇博客->NSURLSession與NSURLConnection區(qū)別)
開啟一個(gè)后臺任務(wù)。
NSCache類:一個(gè)類似于集合的容器。它存儲key-value對,這一點(diǎn)類似于NSDictionary類。我們通常用使用緩存來臨時(shí)存儲短時(shí)間使用但創(chuàng)建昂貴的對象。重用這些對象可以優(yōu)化性能,因?yàn)樗鼈兊闹挡恍枰匦掠?jì)算。另外一方面,這些對象對于程序來說不是緊要的,在內(nèi)存緊張時(shí)會被丟棄。
清理緩存圖片的策略:特別是最大緩存空間大小的設(shè)置。如果所有緩存文件的總大小超過這一大小,則會按照文件最后修改時(shí)間的逆序,以每次一半的遞歸來移除那些過早的文件,直到緩存的實(shí)際大小小于我們設(shè)置的最大使用空間。
對圖片的解壓縮操作:這一操作可以查看SDWebImageDecoder.m中+decodedImageWithImage方法的實(shí)現(xiàn)。
對GIF圖片的處理
對WebP圖片的處理
對cell的重用機(jī)制的解決,利用runtime的關(guān)聯(lián)對象,會為imageView對象關(guān)聯(lián)一個(gè)下載列表,當(dāng)tableView滑動時(shí),imageView重設(shè)數(shù)據(jù)源(url)時(shí),會cancel掉下載列表中所有的任務(wù),然后開啟一個(gè)新的下載任務(wù)。這樣子就保證了只有當(dāng)前可見的cell對象的imageView對象關(guān)聯(lián)的下載任務(wù)能夠回調(diào),不會發(fā)生image錯(cuò)亂。
感興趣的同學(xué)可以深入研究一下這些知識點(diǎn)。當(dāng)然,這只是其中一部分,更多的知識還有待大家去發(fā)掘。
標(biāo)簽
網(wǎng)友跟貼
0人參與
留下你的??吧^_^
快速登錄:
發(fā)表跟貼
最新
最熱
Copyrights ? 2017 貴永冬. All Rights Reserved.