SDWebImage
一個支持遠程服務(wù)器圖片加載緩存的庫
功能簡介
-
UIImageView,UIButton,MKAnnotationView添加Web圖像和緩存管理的類別 - 一個異步圖片下載器
- 一個異步內(nèi)存磁盤圖片緩存且自動處理過期圖片
- 背景圖片壓縮
- 保證同一個URL不會被多次下載
- 保證不會一次又一次地重試偽造的URL
- 保證主線程永遠不會被阻塞
- 性能!
- 使用GCD和ARC
工作流程
- 入口
sd_setImageWithURL:placeholderImage:options:progress:completed:會先取消上次的加載操作,再設(shè)置 placeholderImage 顯示,然后 SDWebImageManager 根據(jù) URL 開始處理圖片。 - 進入 **SDWebImageManager ** 中的
loadImageWithURL:options:progress:completed:,交給 SDImageCache 從緩存中查找圖片 - 先從內(nèi)存緩存查找是否有圖片
imageFromMemoryCacheForKey:,如果內(nèi)存中已經(jīng)有圖片緩存,直接調(diào)用 SDCacheQueryCompletedBlock。 - SDWebImageManager 回調(diào) SDInternalCompletionBlock 到 UIView+WebCache 等前端展示圖片。
- 如果內(nèi)存緩存中沒有,生成
queryDiskBlock添加到隊列中開始從硬盤查找圖片。 - 根據(jù)哈希之后的 URL Key 在磁盤緩存目錄下查找圖片,這一步是根據(jù) SDImageCacheOptions 決定同步查找還是異步在 ioQueue 隊列中查找,查找完成后將圖片添加到內(nèi)存緩存中,然后異步回到主線程中再返回圖片給 SDWebImageManager。
- 如果緩存中獲取不到圖片,則通過 SDWebImageDownloader 下載圖片。
- 如果該URL已存在下載操作 NSOperation<SDWebImageDownloaderOperationInterface> operation (默認為 SDWebImageDownloaderOperation 類型),則將當(dāng)前所對應(yīng)的 progressBlock 和 completedBlock 添加到該 operation 的 callbackBlocks 數(shù)組中,圖片下載由 **NSURLSession ** 來做。
- 在 SDWebImageDownloaderOperation 中的
URLSession:dataTask:didReceiveData:中實現(xiàn)邊下載邊解碼圖片 - 下載完圖片之后,遍歷 callbackBlocks 數(shù)組中的所有完成回調(diào)操作,將下載到的二進制數(shù)據(jù)和圖片返回給 SDWebImageManager,SDWebImageManager 將圖片添加到緩存中。
源碼分析
Cache
減少網(wǎng)絡(luò)請求次數(shù),節(jié)省流量,下載完圖片后存儲到本地,下載再獲取同一個URL時,優(yōu)先從本地獲取,提升用戶體驗。
SDWebImage 對圖片進行緩存工作主要由 SDImageCache 完成。主要用于處理內(nèi)存緩存和磁盤緩存,其中磁盤緩存的寫操作是異步的,不會對UI造成影響。
內(nèi)存緩存
內(nèi)存緩存采用的是 NSCache + NSMapTable 雙重緩存機制,SDMemoryCache 繼承于 NSCache, 會自動處理內(nèi)存緩存問題,并在收到內(nèi)存警告的時候,移除自身所緩存的內(nèi)存資源。但是SDMemoryCache 中的 weakCache 并不會在收到內(nèi)存警告的時候清除。
self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
weakCache 中存儲的 key 值是對URLKey 的強引用,而 value 則是對 UIImage 的弱引用,并不會額外占用內(nèi)存資源。
磁盤緩存
磁盤緩存的處理通過 NSFileManager 對象實現(xiàn),圖片存儲的位置位于 cache 文件夾,還可以設(shè)置 customPaths 數(shù)組來自定義磁盤查詢目錄。另外 SDImageCache 中還有 ioQueue 串行隊列來異步查詢存儲圖片。
存圖片
存儲圖片API:
- (void)storeImage:(nullable UIImage *)image forKey:(nullable NSString *)key completion:(nullable SDWebImageNoParamsBlock)completionBlock;
- (void)storeImage:(nullable UIImage *)image forKey:(nullable NSString *)key toDisk:(BOOL)toDisk completion:(nullable SDWebImageNoParamsBlock)completionBlock;
- (void)storeImage:(nullable UIImage *)image imageData:(nullable NSData *)imageData forKey:(nullable NSString *)key toDisk:(BOOL)toDisk completion:(nullable SDWebImageNoParamsBlock)completionBlock;
- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key;
先保存到內(nèi)存緩存中,同時保存對這張圖片的一個弱引用
/// SDMemoryCache
- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g {
[super setObject:obj forKey:key cost:g];
if (!self.config.shouldUseWeakMemoryCache) {
return;
}
if (key && obj) {
// Store weak cache
LOCK(self.weakCacheLock);
// Do the real copy of the key and only let NSMapTable manage the key's lifetime
// Fixes issue #2507 https://github.com/SDWebImage/SDWebImage/issues/2507
[self.weakCache setObject:obj forKey:[[key mutableCopy] copy]];
UNLOCK(self.weakCacheLock);
}
}
接著異步緩存圖片到磁盤中,根據(jù)圖片類型,通過 SDWebImageCodersManager 將圖片解碼為 NSData 類型,將圖片資源保存到默認的緩存目錄中,文件名為對 key 進行 MD5 后的值。
查圖片
查詢圖片API:
- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key; // 內(nèi)存緩存中查
- (nullable UIImage *)diskImageForKey:(nullable NSString *)key; // 磁盤緩存中查
從 SDImageCache 中查圖片:
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}
// First check the in-memory cache...
UIImage *image = [self imageFromMemoryCacheForKey:key];
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
NSOperation *operation = [NSOperation new];
void(^queryDiskBlock)(void) = ^{
if (operation.isCancelled) {
// do not call the completion if cancelled
return;
}
@autoreleasepool {
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeNone;
if (image) {
// the image is from in-memory cache
diskImage = image;
cacheType = SDImageCacheTypeMemory;
} else if (diskData) {
cacheType = SDImageCacheTypeDisk;
// decode image data only if in-memory cache missed
diskImage = [self diskImageForKey:key data:diskData options:options];
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = diskImage.sd_memoryCost;
[self.memCache setObject:diskImage forKey:key cost:cost];
}
}
if (doneBlock) {
if (options & SDImageCacheQueryDiskSync) {
doneBlock(diskImage, diskData, cacheType);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
}
};
if (options & SDImageCacheQueryDiskSync) {
queryDiskBlock();
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}
return operation;
}
優(yōu)先從內(nèi)存緩存中查找圖片,默認內(nèi)存中查到后不會從磁盤中查,內(nèi)存查不到緩存,則從默認緩存目錄和自定義的查找目錄 customPaths 中遍歷查找。
刪圖片
刪除圖片API:
- (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion;
- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion;
同步從內(nèi)存緩存中刪除圖片,同時對圖片的弱引用也會刪除,磁盤圖片則是異步刪除,磁盤圖片資源只會從默認緩存目錄中刪除,而不會刪除 customPaths 中的圖片資源。
清緩存
清除緩存API:
- (void)clearMemory;
- (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion;
刪除磁盤緩存是異步刪除。
在 iOS 應(yīng)用或 TV 應(yīng)用上,對于一些過期失效的磁盤資源,SDImageCache 會在合適的時機去清除:
- APP 即將銷毀
- APP 已經(jīng)進入后臺
小結(jié)
NSCache + NSMapTable 雙重緩存機制可保證 SDWebImage 內(nèi)部緩存在收到內(nèi)存警告而釋放資源后,還能更快速的從當(dāng)前 APP 的其他地方獲取到這張圖片。customPaths 傳入的目錄數(shù)組僅用于讀操作,默認所有的 io 操作都在 ioQueue 中串行執(zhí)行。
Downloader
SDWebImageDownloader
圖片下載管理器,管理每個圖片下載操作,并控制其生命周期。
- 所有的圖片下載操作都放在 NSOperationQueue 并發(fā)操作隊列 downloadQueue 中,最大并發(fā)數(shù)為6
- 每個 URL 所對應(yīng)的下載操作都放在 URLOperations 中,當(dāng) URLOperations 不存在該 URL 所對應(yīng)的下載操作
id<SDWebImageDownloaderOperationInterface>時,才創(chuàng)建新的下載操作 SDWebImageDownloaderOperation 對象,并存入 URLOperations 中,如果已存在 SDWebImageDownloaderOperation 對象operation,則將 progressBlock 和 completedBlock 保存到 SDWebImageDownloaderOperation 對象的 callbackBlocks 回調(diào)數(shù)組中 - 作為 NSURLSession 和 NSURLSessionDataTask 代理
- 下載操作隊列默認采用 FIFO 先進先出,可設(shè)置為 LIFO 后進先出
- 返回 SDWebImageDownloadToken 對象作為下載操作對象,多次調(diào)用 URL 的下載,返回的 SDWebImageDownloadToken 不同,但其屬性 downloadOperation 卻是同一個下載操作對象
SDWebImageDownloaderOperation
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
LOCK(self.callbacksLock);
[self.callbackBlocks addObject:callbacks];
UNLOCK(self.callbacksLock);
return callbacks;
}
- (BOOL)cancel:(nullable id)token {
BOOL shouldCancel = NO;
LOCK(self.callbacksLock);
[self.callbackBlocks removeObjectIdenticalTo:token];
if (self.callbackBlocks.count == 0) {
shouldCancel = YES;
}
UNLOCK(self.callbacksLock);
if (shouldCancel) {
[self cancel];
}
return shouldCancel;
}
處理 URL 對應(yīng)的具體下載操作,自主管理下載操作狀態(tài)。callbackBlocks 可變數(shù)組存儲每一個回 SDWebImageDownloaderProgressBlock 和 SDWebImageDownloaderCompletedBlock 回調(diào)。
取消下載操作的時候,只是將想要取消的操作所對應(yīng)的 token (即 SDWebImageDownloaderProgressBlock 和 SDWebImageDownloaderCompletedBlock 的鍵值對) 從 callbackBlocks 數(shù)組中移除。
當(dāng)可變數(shù)組 callbackBlocks 中的回調(diào)數(shù)為0的時候,才會取消本次下載操作。
設(shè)置 option 為 SDWebImageDownloaderProgressiveDownload 可邊下載邊回調(diào),正常則在圖片下載完成后,在callCompletionBlocksWithImage:imageData:error:finished:中回調(diào)圖片數(shù)據(jù):
- (void)callCompletionBlocksWithImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
error:(nullable NSError *)error
finished:(BOOL)finished {
NSArray<id> *completionBlocks = [self callbacksForKey:kCompletedCallbackKey];
dispatch_main_async_safe(^{
for (SDWebImageDownloaderCompletedBlock completedBlock in completionBlocks) {
completedBlock(image, imageData, error, finished);
}
});
}
如果存在多個回調(diào),則按照添加的順序回調(diào)的。
小結(jié)
對同一個 URL 的多次下載操作,只會生成一個 operation 對象,只有當(dāng) URL 無對應(yīng)回調(diào)時,才會真正取消該下載操作
主體 Utils
SDWebImageCombinedOperation
內(nèi)存緩存查詢操作和圖片下載操作的結(jié)合體,即包含了 SDWebImage 框架獲取一張圖片的2個主要耗時操作。
-
NSOperation *cacheOperation耗時的磁盤查詢操作, -
SDWebImageDownloadToken *downloadToken網(wǎng)絡(luò)圖片下載操作
SDWebImageManager
在我們的平時使用中,很少直接操作SDWebImageDownloader和SDImageCache去下載保存圖片,大都是通過SDWebImageManager來管理,即使通過UIImageView+WebCache等分類加載圖片,最后也會使用SDWebImageManager來處理。
加載圖片的方法為 loadImageWithURL:options:progress:completed:
判斷 URL 的長度是否大于0;URL 是否在 failedURLs 集合中(存放網(wǎng)絡(luò)資源異常的 URL 集合),若在,options 是否包含 SDWebImageRetryFailed
創(chuàng)建一個 SDWebImageCombinedOperation 對象,保存在 runningOperations 集合中;
從內(nèi)存緩存 SDImageCache 中查詢圖片,并將返回的 NSOperation 賦值給 SDWebImageCombinedOperation 對象的 cacheOperation 屬性
當(dāng)沒緩存圖片或要求刷新數(shù)據(jù)的時候,通過 SDWebImageDownloader 下載圖片,并將返回的 SDWebImageDownloadToken 對象賦值給 SDWebImageCombinedOperation 對象的 downloadToken 屬性
SDWebImagePrefetcher
批預(yù)下載管理器,提前下載一批 URLs 所對應(yīng)的圖片,每次只能處理一批圖片組。使用的圖片管理器并不是 SDWebImageManager 單例,而且單獨創(chuàng)建的實例對象;可設(shè)置最大并發(fā)數(shù),默認為3。
主要用于提前下載圖片數(shù)據(jù),不依賴于 UI 層。
SDWebImageTransition
設(shè)置圖片的過渡效果
Decoder
圖片解碼,講圖片二進制數(shù)據(jù) NSData 解碼出 UIImage,或?qū)?UIImage 編碼成 NSData
SDWebImageCodersManager
圖片編解碼管理器,可通過 addCoder: 和 removeCoder: 添加或移除解碼器,coders 可變數(shù)組用于存放當(dāng)前的所有解碼器。默認只有 SDWebImageImageIOCoder 解碼器
- (BOOL)canEncodeToFormat:(SDImageFormat)format {
LOCK(self.codersLock);
NSArray<id<SDWebImageCoder>> *coders = self.coders;
UNLOCK(self.codersLock);
for (id<SDWebImageCoder> coder in coders.reverseObjectEnumerator) {
if ([coder canEncodeToFormat:format]) {
return YES;
}
}
return NO;
}
通過 SDWebImageCodersManager 編解碼圖片的時候,根據(jù)圖片的二進制數(shù)據(jù)的第一個字節(jié),獲取圖片格式類型,逆遍歷 coders 中的所有解碼器,直到遇到可以成功解碼該格式的解碼器為止。
SDWebImageImageIOCoder
支持 PNG, JPEG, TIFF 格式,同時也支持 GIF 格式,但是只會解碼出第一幀的圖片
SDWebImageGIFCoder
GIF 格式的專用解碼器,通過 CGImage 遍歷解碼出 GIF 動圖
SDWebImageWebPCoder
WebP 格式的專用解碼器,若想解碼出 WebP 格式的圖片,需要單獨導(dǎo)入 WebP 相關(guān)的庫 pod 'SDWebImage/WebP'
小結(jié)
在 SDWebImage 4.0.0 之前,是可以直接設(shè)置 GIF 動圖的,但是在 4.0.0 之后,加載的 GIF 動圖只顯示第一幀的圖像。有2種方式顯示網(wǎng)絡(luò)上的 GIF 動圖:
- 調(diào)用 SDWebImageCodersManager 的
addCoder:方法注冊 SDWebImageGIFCoder 解碼器 - 再單獨導(dǎo)入 FLAnimatedImage 庫
pod 'SDWebImage/GIF',用 FLAnimatedImageView 替換 UIImageView
第二種方法的性能比第一種高
WebCache Categories
給 UIImageView,UIButton,NSButton,MKAnnotationView 等常用圖片容易擴充異步圖片加載方法。
UIImageView+WebCache 采用 UIView+WebCache 默認的賦值方式(統(tǒng)一當(dāng)成 UIImageView 處理);而 UIButton+WebCache 則自己實現(xiàn)了 setImageBlock,MKAnnotationView+WebCache 也是自己實現(xiàn)了 setImageBlock
UIView+WebCache
UIImageView,UIButton,MKAnnotationView 三個類的分類最后也是調(diào)用到了 UIView+WebCache 的sd_internalSetImageWithURL:placeholderImage:options:operationKey:internalSetImageBlock:progress:completed:context: 方法上:
- 根據(jù) operationKey (默認為對象類名)先將上次的加載圖片操作
id<SDWebImageOperation> operation取消 - 設(shè)置占位圖片
- 通過 SDWebImageManager 圖片管理器加載圖片
- 將 SDWebImageManager 返回的
id <SDWebImageOperation> operation和當(dāng)前操作符 operationKey 綁定保存在 sd_operationDictionary 可變哈希映射表中
UIImageView+WebCache
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_internalSetImageWithURL:url
placeholderImage:placeholder
options:options
operationKey:nil
setImageBlock:nil
progress:progressBlock
completed:completedBlock];
}
對應(yīng)的操作符 operationKey 為類名,而setImageBlock 也是 UIView+WebCache 默認以 UIImageView 處理
iOS 應(yīng)用和 TV 應(yīng)用
- (void)sd_setAnimationImagesWithURLs:(nonnull NSArray<NSURL *> *)arrayOfURLs {
[self sd_cancelCurrentAnimationImagesLoad];
NSPointerArray *operationsArray = [self sd_animationOperationArray];
[arrayOfURLs enumerateObjectsUsingBlock:^(NSURL *logoImageURL, NSUInteger idx, BOOL * _Nonnull stop) {
__weak __typeof(self) wself = self;
id <SDWebImageOperation> operation = [[SDWebImageManager sharedManager] loadImageWithURL:logoImageURL options:0 progress:nil completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
__strong typeof(wself) sself = wself;
if (!sself) return;
dispatch_main_async_safe(^{
[sself stopAnimating];
if (sself && image) {
NSMutableArray<UIImage *> *currentImages = [[sself animationImages] mutableCopy];
if (!currentImages) {
currentImages = [[NSMutableArray alloc] init];
}
// We know what index objects should be at when they are returned so
// we will put the object at the index, filling any empty indexes
// with the image that was returned too "early". These images will
// be overwritten. (does not require additional sorting datastructure)
while ([currentImages count] < idx) {
[currentImages addObject:image];
}
currentImages[idx] = image;
sself.animationImages = currentImages;
[sself setNeedsLayout];
}
[sself startAnimating];
});
}];
@synchronized (self) {
[operationsArray addPointer:(__bridge void *)(operation)];
}
}];
}
static char animationLoadOperationKey;
// element is weak because operation instance is retained by SDWebImageManager's runningOperations property
// we should use lock to keep thread-safe because these method may not be acessed from main queue
- (NSPointerArray *)sd_animationOperationArray {
@synchronized(self) {
NSPointerArray *operationsArray = objc_getAssociatedObject(self, &animationLoadOperationKey);
if (operationsArray) {
return operationsArray;
}
operationsArray = [NSPointerArray weakObjectsPointerArray];
objc_setAssociatedObject(self, &animationLoadOperationKey, operationsArray, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return operationsArray;
}
}
- (void)sd_cancelCurrentAnimationImagesLoad {
NSPointerArray *operationsArray = [self sd_animationOperationArray];
if (operationsArray) {
@synchronized (self) {
for (id operation in operationsArray) {
if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
[operation cancel];
}
}
operationsArray.count = 0;
}
}
}
對于這兩個平臺的應(yīng)用,對 UIImageView 額外新增圖片組的異步加載方法
UIImageView+HighlightedWebCache
- (void)sd_setHighlightedImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
__weak typeof(self)weakSelf = self;
[self sd_internalSetImageWithURL:url
placeholderImage:nil
options:options
operationKey:@"UIImageViewImageOperationHighlighted"
setImageBlock:^(UIImage *image, NSData *imageData) {
weakSelf.highlightedImage = image;
}
progress:progressBlock
completed:completedBlock];
}
對應(yīng)的操作符 operationKey 為 UIImageViewImageOperationHighlighted,而setImageBlock 則是自定義
UIButton+WebCache
- (void)sd_setImageWithURL:(nullable NSURL *)url
forState:(UIControlState)state
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
completed:(nullable SDExternalCompletionBlock)completedBlock {
if (!url) {
[self.sd_imageURLStorage removeObjectForKey:imageURLKeyForState(state)];
} else {
self.sd_imageURLStorage[imageURLKeyForState(state)] = url;
}
__weak typeof(self)weakSelf = self;
[self sd_internalSetImageWithURL:url
placeholderImage:placeholder
options:options
operationKey:imageOperationKeyForState(state)
setImageBlock:^(UIImage *image, NSData *imageData) {
[weakSelf setImage:image forState:state];
}
progress:nil
completed:completedBlock];
}
- (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url
forState:(UIControlState)state
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
completed:(nullable SDExternalCompletionBlock)completedBlock {
if (!url) {
[self.sd_imageURLStorage removeObjectForKey:backgroundImageURLKeyForState(state)];
} else {
self.sd_imageURLStorage[backgroundImageURLKeyForState(state)] = url;
}
__weak typeof(self)weakSelf = self;
[self sd_internalSetImageWithURL:url
placeholderImage:placeholder
options:options
operationKey:backgroundImageOperationKeyForState(state)
setImageBlock:^(UIImage *image, NSData *imageData) {
[weakSelf setBackgroundImage:image forState:state];
}
progress:nil
completed:completedBlock];
}
UIButton 的 image 和 backgroundImage 所對應(yīng)的 operationKey 根據(jù)不同的狀態(tài) state 而不同,setImageBlock 也不一樣
Other
MKAnnotationView 的做法和 UIImageView 基本一致的,而 NSButton 則是和 UIButton 基本一致。
End
本文是對 SDWebImage 簡單用法所涉及到的類進行一些簡單的源碼解析。這次的分析是基于 4.3.0 的解析