1. 前言
大名鼎鼎SDWebImage
不用多說,相信每一個iOS程序員或多或少都有了解。比如我,之前就大概只知道是個什么東西,基本屬于沒用過的狀態。最近抽空學習了一下源碼,在此記錄下。
在GitHub上,SDWebImage
描述為Asynchronous image downloader with cache support as a UIImageView category
,翻譯成中文是“UIImageView的一個category,支持緩存的異步圖片下載器”。
可以在該鏈接中查看到最新的文檔https://sdwebimage.github.io
本文使用的源碼為SDWebImage 5.0+版本:
2. 架構
在GitHub上,SDWebImage
提供了非常詳細的架構圖、類圖和順序圖,其中下圖是整體的架構圖
這個圖中可以看到總體包括以下幾個部分
- 基礎組件:包括工具類、分類方法、Image Coder(圖片編碼/解碼)、Image Transformer(圖片轉換)
-
頂層組件:
- Image Manager:負責處理Image Cache(處理圖片緩存和落地)和Image Loader(處理圖片的網絡加載)
- View Category:提供對外的API接口,圖片加載動畫和轉場動畫等。
- Image Prefetcher:圖片的預加載器,是相對比較獨立的部分。
可以看到,SDWebImage
提供了圖片緩存的能力、網絡加載的能力,還包括一些圖片的處理。
順序圖
通過順序圖,可以清楚的看到整個接口的調用流程。
- 需要加載圖片時,
Other Object
只需要調用``UIImageView+WebCahce中的
sd_setImageWithURL()`方法即可 -
sd_setImageWithURL()
會調用UIVIew+WebCache
中的內部加載方法sd_internalSetImageWithURL()
- 接下來會調用
SDWebImageManager
的loadImage()
方法,可以看到,主要的邏輯都在這個SDWebImageManager
中 -
SDWebImageManager
會分別調用SDImageCache
加載緩存數據,然后調用SDWebImageDownloader
從網絡中加載圖片 - 加載完成后,會回調回
UIImageView
中,設置圖片
對于使用者來說,復雜的邏輯都隱藏在SDWebImageManager
之后,還有一些更詳細的類圖,有興趣的可以直接到GitHub的ReadMe去查看。
3. View Category
3.1 WebCache
SDWebImage
提供了以下幾個Category可以方便的完成圖片加載
UIImageView+HighlightedWebCache
UIImageView+WebCache
UIButton+WebCache
NSButton+WebCache
UIView+WebCache
主要的處理邏輯,最終都會調用UIView+WebCache
的下述接口:
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock;
該方法非常長,主要的流程如下:
- 取消在進行的
operation
。- 該
operation
存儲在由UIView+WebCacheOperation
中維護的字典SDOperationsDictionary
中,默認使用當前類名作為operation
的key,其中value是weak指針,因為該operation
由SDWebImageManager
維護
- 該
- 若外部沒有設置
SDWebImageDelayPlaceholder
,則異步在主線程將placeholder
設置到UIImageView
中 - 重置記錄進度的
NSProgress
對象,該對象由當前分類實例維護 - 啟動
ImageIndicator
,默認是一個旋轉菊花,其中iWatch是不支持的 - 接下來就是獲取
SDWebImageManager
了,可以支持外部配置,否則會使用全局唯一的單例 - 設置進度的回調
SDImageLoaderProgressBlock
,該block中,會更新內部的進度條、菊花,然后再回調給外層調用者 - 調用
SDWebImageManager
的加載方法loadImageWithURL:options:context:progress:completed:
,啟動圖片的加載流程 - 在7中方法的
completed
回調中,完成進度更新、關閉菊花、回調completedBlock
以及設置圖片等操作
4. SDWebImageManager
SDWebImageManager
是一個單例類,維護了兩個主要的對象imageCache
和imageLoader
:
@property (strong, nonatomic, readonly, nonnull) id<SDImageCache> imageCache;
@property (strong, nonatomic, readonly, nonnull) id<SDImageLoader> imageLoader;
4.1 加載圖片前的準備工作
主要接口loadImageWithURL:options:context:progress:completed:
的實現邏輯如下:
- 兼容邏輯,若傳進來的url是
NSString
而不是NSURL
,則轉換為NSURL
- 創建一個新的
SDWebImageCombinedOperation
- 判斷是否是已經失敗且不需要重試的url或者url無效,直接回調
completedBlock
返回 - 將
operation
加入到SetrunningOperations
中 - 在執行加載操作前,調用
processedResultForURL
方法,對url
、options
和context
做一次加工操作- 在該方法中,
SDWebImageManager
設置會判斷是否外部有設置transformer
、cacheKeyFilter
和cacheSerializer
, - 最后,會調用外部配置的
optionsProcessor
對象的processedResultForURL
方法,讓使用者有機會修改上述參數
- 在該方法中,
- 調用
callCacheProcessForOperation
方法,開始從緩存中加載圖片
關鍵代碼,代碼中只保留關鍵邏輯:
- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nonnull SDInternalCompletionBlock)completedBlock {
// 1
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// 2
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;
// 3
BOOL isFailedUrl = NO;
if (url) {
SD_LOCK(self.failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
SD_UNLOCK(self.failedURLsLock);
}
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}] url:url];
return operation;
}
// 4
SD_LOCK(self.runningOperationsLock);
[self.runningOperations addObject:operation];
SD_UNLOCK(self.runningOperationsLock);
// 5
SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];
// 6
[self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];
return operation;
}
4.2 從緩存中加載圖片
接口名稱為callCacheProcessForOperation
,該方法中
- 判斷
context
中是否傳入了自定義的SDImageCache,否則使用默認的imageCache
- 判斷
options
是否配置了SDWebImageFromLoaderOnly
,該參數表明,是否僅從網絡加載 - 若僅從網絡中加載,直接調用
callDownloadProcessForOperation
接口,開始下載的步驟 - 否則,獲取url對應的
key
,并調用imageCache
的接口queryImageForKey
,從緩存中加載圖片,在該接口回調中,調用3中的下載接口。
關鍵代碼如下:
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// 1
id<SDImageCache> imageCache;
if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
imageCache = context[SDWebImageContextImageCache];
} else {
imageCache = self.imageCache;
}
// 2
BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
if (shouldQueryCache) {
// 4
NSString *key = [self cacheKeyForURL:url context:context];
@weakify(operation);
operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
@strongify(operation);
if (!operation || operation.isCancelled) {
// Image combined operation cancelled by user
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] url:url];
[self safelyRemoveOperationFromRunning:operation];
return;
}
// Continue download process
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
}];
} else {
// 3
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
}
}
從網絡中加載圖片
接口名為callDownloadProcessForOperation
,實現邏輯如下:
- 與SDImageCache類似,SDImageLoader也支持外部配置,否則使用默認的
imageLoader
- 一系列參數判斷,主要為了判斷是否可以下載
- 當有圖片時,在該方法中可能會先通過
callCompletionBlockForOperation
接口,異步回調completedBlock
設置已經加載好的圖片 - 當判斷可以下載后,會調用
imageLoader
的requestImageWithURL
接口,啟動下載 - 在
requestImageWithURL
的回調中,處理一些失敗等異常邏輯。 - 若加載成功,則通過
callStoreCacheProcessForOperation
接口,將下載的圖片緩存到本地 - 當不需要下載時,會直接返回,若有緩存則會帶上緩存的圖片。
關鍵代碼:
- (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(SDWebImageContext *)context
cachedImage:(nullable UIImage *)cachedImage
cachedData:(nullable NSData *)cachedData
cacheType:(SDImageCacheType)cacheType
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// 1
id<SDImageLoader> imageLoader;
if ([context[SDWebImageContextImageLoader] conformsToProtocol:@protocol(SDImageLoader)]) {
imageLoader = context[SDWebImageContextImageLoader];
} else {
imageLoader = self.imageLoader;
}
// 2
BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly);
shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached);
shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
shouldDownload &= [imageLoader canRequestImageForURL:url];
if (shouldDownload) {
if (cachedImage && options & SDWebImageRefreshCached) {
// 3
[self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
// 將cachedImage傳到image loader中用于檢查是否是相同的圖片
mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage;
context = [mutableContext copy];
}
// 4
@weakify(operation);
operation.loaderOperation = [imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
@strongify(operation);
// 5
if {
// 一系列失敗邏輯
} else {
// 6
[self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
}
}];
} else if (cachedImage) { // 7
[self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
} else {
[self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
}
}
該方法中第6步從網絡拉取成功后,會調用callStoreCacheProcessForOperation
方法將圖片緩存到本地,以及通過調用者提供的SDImageTransformer
轉換圖片。
緩存圖片
調用者提供兩種自定義操作:
- 自定義的
SDImageTransformer
將圖片轉換成另一個圖片 - 自定義的
SDWebImageCacheSerializer
將圖片序列化為NSData
具體邏輯如下:
- 如果有提供
SDWebImageCacheSerializer
,則會先調用接口將圖片序列化之后,再調用存儲接口緩存圖片。注意這一步是放在global_queue
中執行的,不會阻塞主線程,同時使用autoreleasepool
保證NSData能第一時間釋放。 - 第1步結束后,調用
storeImage
接口,通過imageCache
對象將圖片緩存到本地。默認該操作是放在imageCache
維護的io隊列中執行的。 - 最后一步操作,則是調用
callTransformProcessForOperation
接口,轉換圖片。
關鍵代碼:
- (void)callStoreCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(SDWebImageContext *)context
downloadedImage:(nullable UIImage *)downloadedImage
downloadedData:(nullable NSData *)downloadedData
finished:(BOOL)finished
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// 默認拉回來的圖片就是originImage,當提供了transformer轉化圖片時,可以選擇將原圖片和轉換后的圖片都緩存起來
NSString *key = [self cacheKeyForURL:url context:context];
id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
id<SDWebImageCacheSerializer> cacheSerializer = context[SDWebImageContextCacheSerializer];
// 這里會緩存原圖,如果轉換只要完成下載,始終緩存原圖
if (shouldCacheOriginal) {
SDImageCacheType targetStoreCacheType = shouldTransformImage ? originalStoreCacheType : storeCacheType;
if (cacheSerializer && (targetStoreCacheType == SDImageCacheTypeDisk || targetStoreCacheType == SDImageCacheTypeAll)) {
// 1 放到全局隊列中異步序列化
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
NSData *cacheData = [cacheSerializer cacheDataWithImage:downloadedImage originalData:downloadedData imageURL:url];
[self storeImage:downloadedImage imageData:cacheData forKey:key cacheType:targetStoreCacheType options:options context:context completion:^{
[self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
}];
}
});
} else {
// 2
[self storeImage:downloadedImage imageData:downloadedData forKey:key cacheType:targetStoreCacheType options:options context:context completion:^{
[self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
}];
}
} else {
[self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData finished:finished progress:progressBlock completed:completedBlock];
}
}
轉換圖片
如果外部有設置SDImageTransformer
,則會判斷是否需要將轉換后的圖片也緩存起來,關鍵代碼:
- (void)callTransformProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(SDWebImageContext *)context
originalImage:(nullable UIImage *)originalImage
originalData:(nullable NSData *)originalData
finished:(BOOL)finished
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// the target image store cache type
NSString *key = [self cacheKeyForURL:url context:context];
id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
id<SDWebImageCacheSerializer> cacheSerializer = context[SDWebImageContextCacheSerializer];
if (shouldTransformImage) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@autoreleasepool {
UIImage *transformedImage = [transformer transformedImageWithImage:originalImage forKey:key];
if (transformedImage && finished) {
if (cacheSerializer && (storeCacheType == SDImageCacheTypeDisk || storeCacheType == SDImageCacheTypeAll)) {
cacheData = [cacheSerializer cacheDataWithImage:transformedImage originalData:(imageWasTransformed ? nil : originalData) imageURL:url];
} else {
cacheData = (imageWasTransformed ? nil : originalData);
}
// keep the original image format and extended data
SDImageCopyAssociatedObject(originalImage, transformedImage);
[self storeImage:transformedImage imageData:cacheData forKey:cacheKey cacheType:storeCacheType options:options context:context completion:^{
[self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}];
} else {
[self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
}
});
} else {
[self callCompletionBlockForOperation:operation completion:completedBlock image:originalImage data:originalData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
}
加載完成
一切完成后,會通過callCompletionBlockForOperation
回調到最外層的調用者。
5. SDImageCache
關于SDIMageCache,可以直接看以下類圖,用協議定義了所有關鍵類,包括SDImageCache
、SDMemoryCache
、SDDiskCache
。
5.1 SDImageCache
- 持有
SDMemoryCache
和SDDiskCache
,用于從內存和硬盤中加載圖片。可以通過SDImageCacheConfig
配置我們自定義實現的Cache
類 - 維護了一個io隊列,所有從硬盤中異步讀取內容的操作均通過該io隊列執行
- 監聽了App進程被系統殺掉和App切換到后臺的通知,清除過期的數據
獲取圖片接口queryCacheOperationForKey
首先判斷外部是否有傳入
transformer
對象,若有,則會將key
通過SDTransformedKeyForKey
接口將key
和tranformerKey
拼接在一起得到新的key
通過
memoryCache
從內存中獲取圖片,默認情況下,如果獲取到圖片,則直接返回若設置了
SDImageCacheQueryMemoryData
參數,則仍然從硬盤中加載圖片的data
數據。默認異步從硬盤加載,可通過設置參數同步加載加載完成后,通過
block
同步或異步返回
有兩處細節需要注意:
- 使用
@autoreleasepool
保證大的內存占用可以快速釋放 - 異步加載時,使用
io
隊列。異步回調block
時,使用主線程回調
關鍵代碼:
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
// 1
id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
if (transformer)
NSString *transformerKey = [transformer transformerKey];
key = SDTransformedKeyForKey(key, transformerKey);
}
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
// 處理SDImageCacheDecodeFirstFrameOnly或SDImageCacheMatchAnimatedImageClass的邏輯
}
// 2
BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryMemoryData));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}
// 3
NSOperation *operation = [NSOperation new];
// 檢查是否需要同步查詢disk
// 1. 內存緩存命中且設置了同步
// 2. 內存緩存沒有命中但設置了同步讀取硬盤數據
BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
(!image && options & SDImageCacheQueryDiskDataSync));
void(^queryDiskBlock)(void) = ^{
if (operation.isCancelled) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return;
}
@autoreleasepool {
// 從硬盤中加載圖片的data
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeNone;
if (image) { // 內存中已經有圖片,但是需要圖片data
diskImage = image;
cacheType = SDImageCacheTypeMemory;
} else if (diskData) {
cacheType = SDImageCacheTypeDisk;
// 將imageData轉換成image
diskImage = [self diskImageForKey:key data:diskData options:options context:context];
// 將圖片緩存到內存中
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = diskImage.sd_memoryCost;
[self.memoryCache setObject:diskImage forKey:key cost:cost];
}
}
if (doneBlock) {
if (shouldQueryDiskSync) {
doneBlock(diskImage, diskData, cacheType);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
}
};
// 4
if (shouldQueryDiskSync) {
dispatch_sync(self.ioQueue, queryDiskBlock);
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}
return operation;
}
存儲圖片接口storeImage
外部可設置不同的SDImageCacheType
,決定是否需要緩存到內存以及硬盤中
內存緩存:根據
shouldCacheImagesInMemory
接口判斷是否要緩存到內存中-
硬盤緩存:
- 首次將圖片轉換為
NSData
,使用SDAnimatedImage
接口或者SDImageCodersManager
將圖片轉化為NSData
- 通過
diskCache
存儲NSData
到硬盤中 - 檢查圖片是否有
sd_extendedObject
,如果有則也會存儲到硬盤中,使用了NSKeyedArchiver
將sd_extendedObject
轉換為NSData
-
NSKeyedArchiver
的在iOS 11上提供了新的接口archivedDataWithRootObject:requiringSecureCoding:error
- 這里為了兼容iOS 11以下的系統,使用了舊的接口
archivedDataWithRootObject:
,通過clang diagnostic ignored "-Wincompatible-pointer-types"
屏蔽了方法過期警告;使用try catch
捕獲異常 - 通過
diskCache
的setExtendedData
將擴展數據存儲到硬盤中
-
- 首次將圖片轉換為
關鍵代碼:
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toMemory:(BOOL)toMemory
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
// 1
if (toMemory && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = image.sd_memoryCost;
[self.memoryCache setObject:image forKey:key cost:cost];
}
// 2
if (toDisk) {
// 使用iO隊列異步存儲
dispatch_async(self.ioQueue, ^{
@autoreleasepool {
// 2.1
NSData *data = imageData;
if (!data && image) {
data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
}
// 2.2
[self _storeImageDataToDisk:data forKey:key];
if (image) {
// 2.3
id extendedObject = image.sd_extendedObject;
if ([extendedObject conformsToProtocol:@protocol(NSCoding)]) {
NSData *extendedData;
// 2.3.1
if (@available(iOS 11, tvOS 11, macOS 10.13, watchOS 4, *)) {
NSError *error;
extendedData = [NSKeyedArchiver archivedDataWithRootObject:extendedObject requiringSecureCoding:NO error:&error];
if (error) {
NSLog(@"NSKeyedArchiver archive failed with error: %@", error);
}
} else {
// 2.3.2
@try {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
extendedData = [NSKeyedArchiver archivedDataWithRootObject:extendedObject];
#pragma clang diagnostic pop
} @catch (NSException *exception) {
NSLog(@"NSKeyedArchiver archive failed with exception: %@", exception);
}
}
if (extendedData) { // 2.3.4
[self.diskCache setExtendedData:extendedData forKey:key];
}
}
}
}
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
} else {
if (completionBlock) {
completionBlock();
}
}
}
5.2 SDMemoryCache
SDMemoryCache
繼承自NSCache
,其內部做了如下一些事情:
持有一個NSMapTable
的weakCache
該weakCache
的key
為strong
類型,value
為weak
類型,在緩存圖片時,weakCache
也會緩存一份圖片的key
和value
。
這么做的目的是,當NSCache
因內存警告清除了緩存內容后,如果有圖片在App某些地方仍然被引用,那么就可以通過weakCache
來快速加入到NSCache
中,從而阻止了重復從硬盤中讀取。
weakCacheLock
使用了GCD的dispatch_semaphore_t
信號量方式,保證多線程操作weakCache
時的安全性。
5.3 SDDiskCache
SDDiskCache
內部通過NSFileManager
實現了文件的讀寫。值得注意的是
- 存儲文件到硬盤時,
SDDiskCache
會將存儲的key
轉換成md5值后存入本地。 - 清理過期數據邏輯,總共分兩個步驟
- 第一個步驟:根據
SDImageCacheConfigExpireType
設定的排序依據,刪除超過設定的過期時間的文件。 - 在遍歷所有文件時,計算當前存儲文件的總大小。
- 第二個步驟:當存儲的總大小超過設定的總大小時,按照
SDImageCacheConfigExpireType
設定的時間排序,刪除文件,直到設定大小的1/2為止。 - 清除文件的時機是在App退出或退到后臺時,由
SDImageCache
調用。
- 第一個步驟:根據
- 存儲
extendData
:使用了系統的庫<sys/xattr.h>
,通過setxattr
,getxattr
,removexattr
實現了extendData
的設置、讀取、移除操作。
清理過期數據的關鍵代碼:
- (void)removeExpiredData {
NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];
NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];
// 刪除過期的文件
NSDate *modifiedDate = resourceValues[cacheContentDateKey];
if (expirationDate && [[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}
// 存儲文件屬性為后邊的文件大小檢查做準備
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
cacheFiles[fileURL] = resourceValues;
}
for (NSURL *fileURL in urlsToDelete) {
[self.fileManager removeItemAtURL:fileURL error:nil];
}
// 若剩余的文件大小仍然超過了設定的最大值,那么執行第二步步驟。優先刪除更早的文件
NSUInteger maxDiskSize = self.config.maxDiskSize;
if (maxDiskSize > 0 && currentCacheSize > maxDiskSize) {
// 目標是刪除到最大值的一半
const NSUInteger desiredCacheSize = maxDiskSize / 2;
// 按時間排序
NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[cacheContentDateKey] compare:obj2[cacheContentDateKey]];
}];
// 刪除文件直到剩余大小是最大值的一半
for (NSURL *fileURL in sortedFiles) {
if ([self.fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;
if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}
}
6. SDImageLoader
SDImageLoader
的類圖如下,該模塊主要處理網絡請求邏輯。
6.1 SDWebImageDownloader
SDWebImageDownloader
是SDWebImage
提供的圖片下載器類,實現了SDImageLoader
協議。提供了一些配置參數以及多個下載圖片接口。
@interface SDWebImageDownloader : NSObject
@property (nonatomic, copy, readonly) SDWebImageDownloaderConfig *config;
@property (nonatomic, strong) id<SDWebImageDownloaderRequestModifier> requestModifier;
@property (nonatomic, strong) id<SDWebImageDownloaderResponseModifier> responseModifier;
@property (nonatomic, strong) id<SDWebImageDownloaderDecryptor> decryptor;
@property (nonatomic, readonly) NSURLSessionConfiguration *sessionConfiguration;
- (SDWebImageDownloadToken *)downloadImageWithURL:(NSURL *)url completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
- (SDWebImageDownloadToken *)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
- (SDWebImageDownloadToken *)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options context:(SDWebImageContext *)context progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock;
#pragma mark - Protocol<SDImageLoader>
- (id<SDWebImageOperation>)requestImageWithURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context progress:(SDImageLoaderProgressBlock)progressBlock completed:(SDImageLoaderCompletedBlock)completedBlock;
一些關鍵的參數如下:
downloadQueue
:NSOperationQueue
類型,用于執行每一個下載任務創建的NSOperation
;URLOperations
:字典類型,key
為URL,value
是NSOperation<SDWebImageDownloaderOperation>
,使用該對象來維護SDWebImageDownloader
生命周期內所有網絡請求的Operation
對象。session
:使用外部或者默認的sessionConfiguration
創建的NSURLSession
對象。
圖片下載核心流程
核心圖片下載方法為downloadImageWithURL
,主要流程如下:
判斷
URLOperations
是否已經緩存該url
對應的NSOperation<SDWebImageDownloaderOperation>
對象若已經存在
operation
,將該方法傳入的progressBlock
和completedBlock
加入到operation
中,同時若該operation
還未被執行時,會根據傳入的options
調整當前queue
的優先級。若
operation
不存在、已經完成或者被取消,通過createDownloaderOperationWithUrl
方法創建一個新的operation
。operation
創建成功,設置completionBlock
,添加operation
到URLOperations
中,調用addHandlersForProgress
添加progressBlock
和completedBlock
,最后,將operation
添加到downloadQueue
(根據蘋果文檔,在添加operation
到queue
之前,需要執行完所有配置)。最后,創建并返回
SDWebImageDownloadToken
對象,該對象包含了url
、request
、以及downloadOperationCancelToken
。`
關鍵代碼:
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url options:(SDWebImageDownloaderOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
id downloadOperationCancelToken;
// 1
NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
// 3
if (!operation || operation.isFinished || operation.isCancelled) {
operation = [self createDownloaderOperationWithUrl:url options:options context:context];
@weakify(self);
operation.completionBlock = ^{
@strongify(self);
if (!self) {
return;
}
[self.URLOperations removeObjectForKey:url];
};
self.URLOperations[url] = operation;
// 4
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
[self.downloadQueue addOperation:operation];
} else { // 2
@synchronized (operation) {
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
}
if (!operation.isExecuting) {
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
} else {
operation.queuePriority = NSOperationQueuePriorityNormal;
}
}
}
// 5
SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
token.url = url;
token.request = operation.request;
token.downloadOperationCancelToken = downloadOperationCancelToken;
return token;
}
SDWebImageDownloadToken
實現了SDWebImageOperation
協議,對于外部調用者來說,可以通過id<SDWebImageOperation>
取消當前操作,定義如下:
@interface SDWebImageDownloadToken : NSObject <SDWebImageOperation>
- (void)cancel;
@property (nonatomic, strong, nullable, readonly) NSURL *url;
@property (nonatomic, strong, nullable, readonly) NSURLRequest *request;
@property (nonatomic, strong, nullable, readonly) NSURLResponse *response;
@property (nonatomic, strong, nullable, readonly) NSURLSessionTaskMetrics *metrics;
@end
創建NSOperation<SDWebImageDownloaderOperation>
對象
NSOperation
通過createDownloaderOperationWithUrl
方法創建,主要流程如下:
- 創建
NSMutableURLRequest
對象,設置緩存策略、是否使用默認Cookies 、Http頭信息等。 - 獲取外部配置的
SDWebImageDownloaderRequestModifier
對象,若沒有則使用self
的,通過modifiedRequestWithRequest
接口在請求之前有機會檢查并修改一次Request
,若返回了nil
,本次請求會終止。 - 外部同樣可以配置
SDWebImageDownloaderResponseModifier
對象,用來修改Response
,這個會先存儲在context
中,等待請求回來后再去調用。 - 獲取
SDWebImageDownloaderDecryptor
對象,同樣是請求回來后,用于解密相關操作。 -
context
參數檢查完畢后,需要創建NSOperation<SDWebImageDownloaderOperation>
對象,此處可以通過設置config
的operationClass
來傳入自定義的類名,若外部沒有傳入,則會使用SDWebImage
提供改的SDWebImageDownloaderOperation
類。 - 設置http請求的證書,首先獲取
config
中的urlCredential
,其次通過config
中的usrname
和password
創建NSURLCredential
對象。 - 設置其他參數,如http請求的證書、最小進度間隔、當前請求的優先級等。
- 如果設置了
SDWebImageDownloaderLIFOExecutionOrder
,表明所有的請求都是LIFO
(后進先出)的執行方式,此處的處理方式是遍歷當前downloadQueue
的operations
,將新的operation
設置為所有operations
的依賴,代碼如下:
關鍵代碼:
- (nullable NSOperation<SDWebImageDownloaderOperation> *)createDownloaderOperationWithUrl:(nonnull NSURL *)url options:(SDWebImageDownloaderOptions)options context:(nullable SDWebImageContext *)context {
NSTimeInterval timeoutInterval = self.config.downloadTimeout;
// 1
NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData; // 默認情況下不使用NSURLCache
NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
mutableRequest.HTTPShouldHandleCookies = SD_OPTIONS_CONTAINS(options, SDWebImageDownloaderHandleCookies);
mutableRequest.HTTPShouldUsePipelining = YES;
mutableRequest.allHTTPHeaderFields = self.HTTPHeaders;
// 2
id<SDWebImageDownloaderRequestModifier> requestModifier;
if ([context valueForKey:SDWebImageContextDownloadRequestModifier]) {
requestModifier = [context valueForKey:SDWebImageContextDownloadRequestModifier];
} else {
requestModifier = self.requestModifier;
}
NSURLRequest *request;
if (requestModifier) {
NSURLRequest *modifiedRequest = [requestModifier modifiedRequestWithRequest:[mutableRequest copy]];
} else {
request = [mutableRequest copy];
}
// 3
id<SDWebImageDownloaderResponseModifier> responseModifier;
if ([context valueForKey:SDWebImageContextDownloadResponseModifier]) {
responseModifier = [context valueForKey:SDWebImageContextDownloadResponseModifier];
} else {
responseModifier = self.responseModifier;
}
if (responseModifier) {
mutableContext[SDWebImageContextDownloadResponseModifier] = responseModifier;
}
// 4
id<SDWebImageDownloaderDecryptor> decryptor;
if ([context valueForKey:SDWebImageContextDownloadDecryptor]) {
decryptor = [context valueForKey:SDWebImageContextDownloadDecryptor];
} else {
decryptor = self.decryptor;
}
if (decryptor) {
mutableContext[SDWebImageContextDownloadDecryptor] = decryptor;
}
context = [mutableContext copy];
// 5
Class operationClass = self.config.operationClass;
if (operationClass && [operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperation)]) {
} else {
operationClass = [SDWebImageDownloaderOperation class];
}
NSOperation<SDWebImageDownloaderOperation> *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context];
// 6
if ([operation respondsToSelector:@selector(setCredential:)]) {
if (self.config.urlCredential) {
operation.credential = self.config.urlCredential;
} else if (self.config.username && self.config.password) {
operation.credential = [NSURLCredential credentialWithUser:self.config.username password:self.config.password persistence:NSURLCredentialPersistenceForSession];
}
}
// 7
if ([operation respondsToSelector:@selector(setMinimumProgressInterval:)]) {
operation.minimumProgressInterval = MIN(MAX(self.config.minimumProgressInterval, 0), 1);
}
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
// 8
if (self.config.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
for (NSOperation *pendingOperation in self.downloadQueue.operations) {
[pendingOperation addDependency:operation];
}
}
return operation;
}
6.2 SDWebImageDownloaderOperation
在前邊的SDWebImageDownloader
初始化時,可以看到創建了NSURLSession
對象,且delegate
設置的為self
,但實際上,當SDWebImageDownloader
接收到NSURLSessionTaskDelegate
或者NSURLSessionDataDelegate
回調時,都會轉發到對應的NSOperation<SDWebImageDownloaderOperation>
對象去處理,默認情況,就是SDWebImageDownloaderOperation
。來看看這里的主要流程吧。
啟動方法start
通過
beginBackgroundTaskWithExpirationHandler
方法申請在進入后臺后,更多的時間執行下載任務。判斷
session
,該類中有兩個session
,unownedSession
(外部傳入),ownedSession
(內部創建),當外部沒有傳入session
時,內部則會再創建一個,保證任務可以繼續執行。保存緩存數據,如果設置了
SDWebImageDownloaderIgnoreCachedResponse
時,當拉取回來的數據和已緩存的數據一致,就回調上層nil
,這里保存的緩存數據用于拉取結束后的判斷。通過
dataTaskWithRequest
創建NSURLSessionTask
對象dataTask
。設置
dataTask
和coderQueue
的優先級。啟動本次任務,通過
progressBlock
回調當前進度,這里block
可以存儲多個,外部通過addHandlersForProgress
方法添加。這里還會再在主線程拋一個啟動的通知
SDWebImageDownloadStartNotification
。
關鍵代碼
- (void)start {
// 1
#if SD_UIKIT
Class UIApplicationClass = NSClassFromString(@"UIApplication");
BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
__weak typeof(self) wself = self;
UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
[wself cancel];
}];
}
#endif
// 2
NSURLSession *session = self.unownedSession;
if (!session) {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
session = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
self.ownedSession = session;
}
// 3
if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
NSURLCache *URLCache = session.configuration.URLCache;
if (!URLCache) {
URLCache = [NSURLCache sharedURLCache];
}
NSCachedURLResponse *cachedResponse;
@synchronized (URLCache) {
cachedResponse = [URLCache cachedResponseForRequest:self.request];
}
if (cachedResponse) {
self.cachedData = cachedResponse.data;
}
}
// 4
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
}
// 5
if (self.dataTask) {
if (self.options & SDWebImageDownloaderHighPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityHigh;
self.coderQueue.qualityOfService = NSQualityOfServiceUserInteractive;
} else if (self.options & SDWebImageDownloaderLowPriority) {
self.dataTask.priority = NSURLSessionTaskPriorityLow;
self.coderQueue.qualityOfService = NSQualityOfServiceBackground;
} else {
self.dataTask.priority = NSURLSessionTaskPriorityDefault;
self.coderQueue.qualityOfService = NSQualityOfServiceDefault;
}
// 6
[self.dataTask resume];
for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
}
__block typeof(self) strongSelf = self;
// 7
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:strongSelf];
});
} else {
[self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
[self done];
}
}
NSURLSessionTaskDelegate
和NSURLSessionDataDelegate
在SDWebImageDownloaderOperation
中,實現了NSURLSession
的delegate
回調處理,具體邏輯比較多且不復雜,就不在這里贅述,可自行查閱代碼。
7. 一些細節
7.1 宏定義
多平臺適配
SDWebImage
中多處地方使用了平臺宏去區分不同平臺的特性,對于想要了解跨平臺的一些特性,非常有借鑒意義。
// iOS and tvOS are very similar, UIKit exists on both platforms
// Note: watchOS also has UIKit, but it's very limited
#if TARGET_OS_IOS || TARGET_OS_TV
#define SD_UIKIT 1
#else
#define SD_UIKIT 0
#endif
#if TARGET_OS_IOS
#define SD_IOS 1
#else
#define SD_IOS 0
#endif
#if TARGET_OS_TV
#define SD_TV 1
#else
#define SD_TV 0
#endif
#if TARGET_OS_WATCH
#define SD_WATCH 1
#else
#define SD_WATCH 0
#endif
以及通過宏將Mac平臺的NSImage
聲明為UIImage
,NSImageView
聲明為UIImageView
等,讓一套代碼得以方便你的適配多個平臺不同的控件名稱。
#if SD_MAC
#import <AppKit/AppKit.h>
#ifndef UIImage
#define UIImage NSImage
#endif
#ifndef UIImageView
#define UIImageView NSImageView
#endif
#ifndef UIView
#define UIView NSView
#endif
#ifndef UIColor
#define UIColor NSColor
#endif
#else
#if SD_UIKIT
#import <UIKit/UIKit.h>
#endif
#if SD_WATCH
#import <WatchKit/WatchKit.h>
#ifndef UIView
#define UIView WKInterfaceObject
#endif
#ifndef UIImageView
#define UIImageView WKInterfaceImage
#endif
#endif
#endif
判斷是否主線程dispatch_main_async_safe
#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
#endif
一般情況下,需要判斷是否在主線程,可能會使用NSThread.isMainThread
來判斷,這個就可以滿足大部分的場景了。而SDWebImage
的實現有些不一樣,判斷的方式是當前的queue
是否是主隊列,并沒有判斷當前的線程。
實際上,主線程和主隊列不完全是一個東西,有微小的區別。主線程上也可以運行其他隊列。
在這篇OpenRadar中有提到,在主線程但非主隊列中調用MKMapView
的addOverlay
方法是不安全的。具體可參考下列文章:
7.2 多線程安全
在代碼中有大量的地方使用了鎖去保證多線程安全,包括常見的@synchonzied
以及GCD的信號量
#ifndef SD_LOCK
#define SD_LOCK(lock) dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
#endif
#ifndef SD_UNLOCK
#define SD_UNLOCK(lock) dispatch_semaphore_signal(lock);
#endif
8. 結語
SDWebImage
代碼暫時就講解這么多,不過該庫的功能遠不止于此,非常強大,對于有需要使用的,可以再詳細的去了解具體使用的地方。