前言
這是本系列的第 3 篇,在前一篇中,我們了解了 SDWebImage 執行的基本流程,本篇就來介紹第一個核心類 SDWebImageMananger
。
正文
SDWebImageMananger.h 文件基本可以分為 3 各部分:
①定義了一個枚舉 SDWebImageOptions
,列舉了可能會用到的一些場景。
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
// 重試已經失敗的 url
SDWebImageRetryFailed = 1 << 0,
// 低優先級,比如,在有 UI 交互的情況下,會延遲下載操作
SDWebImageLowPriority = 1 << 1,
// 下載完成后,僅做內存緩存,不做磁盤緩存
SDWebImageCacheMemoryOnly = 1 << 2,
// 下載過程中逐步加載圖片,而不是完全下載完之后才展示
SDWebImageProgressiveDownload = 1 << 3,
// 刷新緩存
SDWebImageRefreshCached = 1 << 4,
// 當 App 進入后臺時,繼續下載任務,如果后臺任務超時,操作將被自動取消
SDWebImageContinueInBackground = 1 << 5,
// 允許處理 Cookie
SDWebImageHandleCookies = 1 << 6,
// 允許不受信任的 SSL 證書,生產環境慎用
SDWebImageAllowInvalidSSLCertificates = 1 << 7,
// 高優先級,即會把相應的圖片放到最前邊加載,而不是按照加入隊列時的順序執行
SDWebImageHighPriority = 1 << 8,
// 延遲 placeholder 的加載,即在下載完成時才加載
SDWebImageDelayPlaceholder = 1 << 9,
// 對動圖也執行 transform 操作
SDWebImageTransformAnimatedImage = 1 << 10,
// 圖片下載完成后,不直接自動給 imageView 賦值,給用戶調整圖片的機會
SDWebImageAvoidAutoSetImage = 1 << 11,
// 依據設備內存縮放圖片,如果設置了 `SDWebImageProgressiveDownload` ,此設置無效
SDWebImageScaleDownLargeImages = 1 << 12,
// 在有內存緩存的情況下,依然需要查詢磁盤緩存,建議與 SDWebImageQueryDiskSync 配合使用
SDWebImageQueryDataWhenInMemory = 1 << 13,
// 同步查詢磁盤緩存
SDWebImageQueryDiskSync = 1 << 14,
// 僅加載緩存圖片
SDWebImageFromCacheOnly = 1 << 15,
// 對內存和磁盤中的 image 也執行 transition 的操作
SDWebImageForceTransition = 1 << 16
};
②定義了一個協議 SDWebImageManagerDelegate
,這里提供了以下 3 個協議方法:
// 緩存中沒有指定圖片時,是否需要下載
- (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldDownloadImageForURL:(nullable NSURL *)imageURL;
// 是否需要將制定 URL 標記為失敗的 URL
- (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldBlockFailedURL:(nonnull NSURL *)imageURL withError:(nonnull NSError *)error;
// 允許在剛剛下載到 image 并且未做緩存之前,對圖片執行 transform,返回處理后的 image
- (nullable UIImage *)imageManager:(nonnull SDWebImageManager *)imageManager transformDownloadedImage:(nullable UIImage *)image withURL:(nullable NSURL *)imageURL;
③SDWebImageManager 的頭文件,有幾個重要屬性,他們的作用見下邊的注釋。
// 代理對象
@property (weak, nonatomic, nullable) id <SDWebImageManagerDelegate> delegate;
// 處理緩存的對象
@property (strong, nonatomic, readonly, nullable) SDImageCache *imageCache;
// 處理下載工作的對象
@property (strong, nonatomic, readonly, nullable) SDWebImageDownloader *imageDownloader;
// 一個用戶定義的 block,用于生成 cacheKey
@property (nonatomic, copy, nullable) SDWebImageCacheKeyFilterBlock cacheKeyFilter;
// 一個用戶定義的 block,用于序列化下載到的數據
@property (nonatomic, copy, nullable) SDWebImageCacheSerializerBlock cacheSerializer;
下面是 2 個常用的創建方法:+ (nonnull instancetype)sharedManager;
和 - (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader;
,其實最終都是調用了后者 。
// 單例
+ (nonnull instancetype)sharedManager {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new];
});
return instance;
}
// 初始化,創建處理緩存和下載任務的對象 cache 和 downloader
- (nonnull instancetype)init {
SDImageCache *cache = [SDImageCache sharedImageCache];
SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
return [self initWithCache:cache downloader:downloader];
}
// 核心的初始化方法,為各屬性賦初值
- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader {
if ((self = [super init])) {
// 處理緩存和下載任務的對象
_imageCache = cache;
_imageDownloader = downloader;
// 用于存儲請求失敗的 URL 的集合及操作時用的鎖 (信號量)
_failedURLs = [NSMutableSet new];
_failedURLsLock = dispatch_semaphore_create(1);
// 存儲運行中 operation 的集合,通過判斷他的 count 是否為 0,判斷操作是否在進行中:BOOL isRunning = (self.runningOperations.count > 0);
_runningOperations = [NSMutableSet new];
// 操作時用的鎖 (信號量)
_runningOperationsLock = dispatch_semaphore_create(1);
}
return self;
}
另外幾個方法,就不單獨介紹了,用到的時候再繼續討論。此處,我們只看一個核心方法 - (nullable id <SDWebImageOperation>)loadImageWithURL:url options:options progress:progressBlock completed:completedBlock;
,下面我們來一步步討論這個方法的具體實現。
1.校驗參數
依次做如下處理:如果傳入的 completedBlock 為空,就直接報錯;如果傳入的參數是 NSString * 類型的,需要將其轉換成 NSURL;最后,如果 url 還不是 NSURL 類型,那就只能將其置為 nil,以免造成后邊 Crash。
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
2.生成總的 operation
他是 SDWebImageCombinedOperation
實例對象,也是當前方法要返回的結果,并將當前類賦值給 operation 的一個 weak 屬性(避免循環引用)。
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self; // 肯定是 weak 屬性
SDWebImageCombinedOperation
的聲明與實現文件均在當前類 SDWebImageManager
的實現文件里邊,簡單看一下他的 .h/.m 文件吧。
// SDWebImageCombinedOperation.h
@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>
// 標識是否已取消
@property (assign, nonatomic, getter = isCanceled) BOOL cancelled;
// downloadToken 這是一個繼承自 NSObject 的類,他有一個繼承自 NSOperation 的屬性,也就是真正執行下載操作時的 operation,cancel 時會用到
@property (strong, nonatomic, nullable) SDWebImageDownloadToken *downloadToken;
// 查詢緩存時的 operation,用于標識當前 operation 是否已經被取消。其實查詢緩存時,首先查看 operation.isCanceled,如果沒被取消了,就會再去查詢了。cancel 時會將其 isCanceled 屬性置為 YES。
@property (strong, nonatomic, nullable) NSOperation *cacheOperation;
// manager
@property (weak, nonatomic, nullable) SDWebImageManager *manager;
@end
// SDWebImageCombinedOperation.m
#pragma mark - 代理方法實現
@implementation SDWebImageCombinedOperation
- (void)cancel {
@synchronized(self) {
self.cancelled = YES;
// 取消查詢緩存的 Operation,此時 isCanceled 會被置為 YES。
if (self.cacheOperation) {
[self.cacheOperation cancel];
self.cacheOperation = nil;
}
// 取消下載操作
if (self.downloadToken) {
[self.manager.imageDownloader cancel:self.downloadToken];
}
// 將當前 operation 從 manager 中運行著的 operation 數組中移除。
[self.manager safelyRemoveOperationFromRunning:self];
}
}
@end
// SDWebImageOperation 協議的定義
@protocol SDWebImageOperation <NSObject>
- (void)cancel;
@end
可以看到,SDWebImageCombinedOperation
這個類的主要作用就是 cancel 操作,包括 cancel 查詢緩存 和 cancel 下載數據。
3.再次檢測一下 url
如果是曾經失敗的 url,而且不允許重試,或者 url 為空時,執行 completionBlock,并返回當前 operation。
// self.failedURLs 是一個保存曾經失敗過的 URL 的數組,用于檢測當前 URL 是不是曾經請求失敗過的URL.另外,搜索一個個元素的時候,NSSet 比 NSArray 查詢更快。
BOOL isFailedUrl = NO;
if (url) {
LOCK(self.failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
UNLOCK(self.failedURLsLock);
}
// 若出現以下兩種情況就不再往下走了,直接執行 CompletionBlock:① URL 是空的;② 此 URL 是曾經請求失敗的 URL,并且規定不允許重新請求曾經失敗的 URL。
if (url.absoluteString.length == 0
|| (!(options & SDWebImageRetryFailed) && isFailedUrl))
{
[self callCompletionBlockForOperation:operation
completion:completedBlock
error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil]
url:url];
return operation;
}
4.保存 operation
至 self.runningOperations
后者是一個數組,這里使用了信號量來確保線程安裝。
LOCK(self.runningOperationsLock);
[self.runningOperations addObject:operation];
UNLOCK(self.runningOperationsLock);
5.查詢緩存。
NSString *key = [self cacheKeyForURL:url];
SDImageCacheOptions cacheOptions = 0;
if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
__weak SDWebImageCombinedOperation *weakOperation = operation;
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key
options:cacheOptions
done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType)
{
// 查詢完成后的操作在這里,可能查到了,也可能沒查到...
}
這里先準備了 2 個參數,查詢的依據 key 和一些條件 cacheOptions。key 的獲取是通過一個私有方法 (如下),如果自定義了 key
的生成規則 self.cacheKeyFilter,就用自定義的,如果沒有,就直接取 url.absoluteString。cacheOptions
是一個用 NS_OPTIONS 定義的枚舉類型 (前邊已介紹過),可組合多種情況,在這里綜合了 2 個查詢的要求和 1 個縮放圖片的要求。
- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url {
if (!url) {
return @"";
}
if (self.cacheKeyFilter) {
return self.cacheKeyFilter(url);
} else {
return url.absoluteString;
}
}
6.查詢的具體過程
詳情將會在 SDImageCache 中介紹,下面討論一下查詢緩存結束后的操作。
7.移除當前 operation
從 self.runningOperations 這個數組中移除當前 operation。
if (!strongOperation || strongOperation.isCancelled) {
[self safelyRemoveOperationFromRunning:strongOperation];
return;
}
8.判斷是否需要下載
當同時滿足 3 個要求時,就需要下載新數據了:
①沒要求只能從緩存獲取數據,即當緩存找不到時,可以去下載;
②找不到緩存 或 要求必須更新緩存;
③當 self.delegate 沒有遵守協議, 或者 協議方法返回 YES。
BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly))
&& (!cachedImage || options & SDWebImageRefreshCached)
&& (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
9.若需要下載
- 首先依然要做一個判斷,即 如果有緩存數據并且要求刷新緩存數據時,需要先調用一次 CompletionBlock,將緩存數據返回去,然后再開始下載新數據,代碼如下:
if (cachedImage && options & SDWebImageRefreshCached) {
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}
然后,準備下載數據時所需的一些基本選項,可以參考篇頭介紹的枚舉 SDWebImageOptions
。
- 開始下載,調用了 SDWebImageDownloader 的下載方法,留待 SDWebImageDownloader 介紹,這里只討論下載完成之后的操作。
__weak typeof(strongOperation) weakSubOperation = strongOperation;
strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url
options:downloaderOptions
progress:progressBlock
completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished)
{
// 下載完成后的操作...
}
下載完成后,可以分這么幾種情況:
a.當前 operation 已經被取消,這種情況下不作任何操作,包括回調。
b.下載出錯,先將失敗的 error 信息返回,然后決定是否需要將當前 URL 存入失敗的 URL 數組。
c.下載成功,此時要做的工作還有許多:
①如果設置了失敗重發,則將當前 URL 從失敗的 URL 數組中移除。
if ((options & SDWebImageRetryFailed)) {
LOCK(self.failedURLsLock);
[self.failedURLs removeObject:url];
UNLOCK(self.failedURLsLock);
}
②對于自定義的 manager,需要執行另外一套縮放標準。
if (self != [SDWebImageManager sharedManager]
&& self.cacheKeyFilter
&& downloadedImage)
{
downloadedImage = [self scaledImageForKey:key image:downloadedImage];
}
③若需要更新緩存,但是未下載到圖片,且緩存中本來有值的情況下,什么也不做,因為下載之前早已經緩存數據返回了。
if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
// 需要更新緩存,但是未下載到圖片,且緩存中本來有值的情況下,什么也不做,因為下載之前早已經緩存數據返回了
}
④如果下載到了圖片,并且要求 transform 圖片的情況下,異步執行 transform 和緩存圖片的工作,然后回到主線程執行 completionBlock。
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];
NSData *cacheData;
// pass nil if the image was transformed, so we can recalculate the data from the image
if (self.cacheSerializer) {
cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
} else {
cacheData = (imageWasTransformed ? nil : downloadedData);
}
// *** 存盤:注意是存的 imageData
[self.imageCache storeImage:transformedImage
imageData:cacheData
forKey:key
toDisk:cacheOnDisk
completion:nil];
}
[self callCompletionBlockForOperation:strongSubOperation
completion:completedBlock
image:transformedImage
data:downloadedData
error:nil
cacheType:SDImageCacheTypeNone
finished:finished
url:url];
});
⑤如果下載到了圖片,并且下載完成的話,則存盤并執行 completionBlock。存盤調用了 SDImageCache
的方法,隨后介紹。
最后將當前 operation 移除。
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];
NSData *cacheData;
// pass nil if the image was transformed, so we can recalculate the data from the image
if (self.cacheSerializer) {
cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
} else {
cacheData = (imageWasTransformed ? nil : downloadedData);
}
// *** 存盤:注意是存的 imageData
[self.imageCache storeImage:transformedImage
imageData:cacheData
forKey:key
toDisk:cacheOnDisk
completion:nil];
}
[self callCompletionBlockForOperation:strongSubOperation
completion:completedBlock
image:transformedImage
data:downloadedData
error:nil
cacheType:SDImageCacheTypeNone
finished:finished
url:url];
});
// ...
if (finished) {
[self safelyRemoveOperationFromRunning:strongSubOperation];
}
10.若不需要下載,并且有緩存
此時,執行 completionBlock 將緩存數據返回,然后移除當前 operation。
[self callCompletionBlockForOperation:strongOperation
completion:completedBlock
image:cachedImage
data:cachedData
error:nil
cacheType:cacheType
finished:YES
url:url];
[self safelyRemoveOperationFromRunning:strongOperation];
11.其它,即沒有緩存,且不需要下載
和上邊的操作類似,只不過傳回的 image 和 data 均為 nil。
[self callCompletionBlockForOperation:strongOperation
completion:completedBlock
image:nil
data:nil
error:nil
cacheType:SDImageCacheTypeNone
finished:YES
url:url];
[self safelyRemoveOperationFromRunning:strongOperation];
最后將 operation 返回。
小結
以上就是 SDWebImageManager 這個類的主要功能,其中關于緩存和下載的內容,詳見后邊幾篇的討論。