前面的兩篇文章簡單分析了SDImageCache和
SDWebImageDownloader的代碼。它們分別實現了圖片的緩存和下載。現在,還需要一個統一的類把它們的功能組合到一起,方便用戶調用。這個類就是SDWebImageManager
。
以下代碼和分析都基于041842bf085cbb711f0d9e099e6acbf6fd533b0c這個commit。
SDWebImageManager
- (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;
_failedURLs = [NSMutableSet new];
_runningOperations = [NSMutableArray new];
}
return self;
}
SDWebImageManager
提供了可以設置指定的SDImageCache
和SDWebImageDownloader
的init
方法,方便調用方根據需要設置自己的SDImageCache
和SDWebImageDownloader
的實例。這種依賴注入的設計方式也可以方便單元測試時注入mock的對象。
/**
* The cache filter is a block used each time SDWebImageManager need to convert an URL into a cache key. This can
* be used to remove dynamic part of an image URL.
*
* The following example sets a filter in the application delegate that will remove any query-string from the
* URL before to use it as a cache key:
*
* @code
[[SDWebImageManager sharedManager] setCacheKeyFilter:^(NSURL *url) {
url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path];
return [url absoluteString];
}];
* @endcode
*/
@property (nonatomic, copy, nullable) SDWebImageCacheKeyFilterBlock cacheKeyFilter;
- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url {
if (!url) {
return @"";
}
if (self.cacheKeyFilter) {
return self.cacheKeyFilter(url);
} else {
return url.absoluteString;
}
}
- (void)diskImageExistsForURL:(nullable NSURL *)url
completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock {
NSString *key = [self cacheKeyForURL:url];
[self.imageCache diskImageExistsWithKey:key completion:^(BOOL isInDiskCache) {
// the completion block of checkDiskCacheForImageWithKey:completion: is always called on the main queue, no need to further dispatch
if (completionBlock) {
completionBlock(isInDiskCache);
}
}];
}
SDImageCache
只管以key為唯一標識保存圖片;SDWebImageDownloader
只管依照圖片URL下載圖片,這里的cacheKeyFilter
決定了URL應該被怎樣處理成圖片緩存用的key。
@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>
@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (copy, nonatomic, nullable) SDWebImageNoParamsBlock cancelBlock;
@property (strong, nonatomic, nullable) NSOperation *cacheOperation;
@end
@interface SDWebImageManager ()
@property (strong, nonatomic, nonnull) NSMutableArray<SDWebImageCombinedOperation *> *runningOperations;
@end
這里設計了一個SDWebImageCombinedOperation
類,并維護了一個數組runningOperations
對其進行管理。
loadImageWithURL
加載圖片有兩個步驟,查詢和下載。SDWebImageCombinedOperation
同時代表了緩存查詢和下載這兩種行為。
這里的流程代碼也比較多,用文字理一下:
-
初始化:
loadImageWithURL
方法被調用時,把SDWebImageCombinedOperation
加到runningOperations
里。 -
查詢:到
SDImageCache
里查詢圖片是否存在,把查詢緩存用的NSOperation
保存在SDWebImageCombinedOperation
里。 -
查詢結束:如果圖片在緩存中,回調
completedBlock
,把SDWebImageCombinedOperation
從runningOperations
里去掉。否則開始下載。 -
下載:調用
SDWebImageDownloader
下載圖片,并且把SDWebImageCombinedOperation
的cancelBlock
設置成取消下載。 -
下載結束:圖片下載完成后,回調
completedBlock
,并把SDWebImageCombinedOperation
從runningOperations
里去掉。
可以看到,只要加載圖片的過程在進行,無論是查詢還是下載,這個任務都會在runningOperations
里,外部可以統一的查詢和管理圖片加載的任務。
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
if (operation.isCancelled) {
[self safelyRemoveOperationFromRunning:operation];
return;
}
...
}];
operation.cancelBlock = ^{
[self.imageDownloader cancel:subOperationToken];
__strong __typeof(weakOperation) strongOperation = weakOperation;
[self safelyRemoveOperationFromRunning:strongOperation];
};
要注意的是,如果在查詢過程中,調用取消任務并不會中止查詢流程,只是在最終返回時直接丟掉查詢結果;而如果在下載過程中,調用cancel
最后會調用到SDWebImageDownloader
的cancel,真正取消下載任務。
- (void)cancelAll {
@synchronized (self.runningOperations) {
NSArray<SDWebImageCombinedOperation *> *copiedOperations = [self.runningOperations copy];
[copiedOperations makeObjectsPerformSelector:@selector(cancel)];
[self.runningOperations removeObjectsInArray:copiedOperations];
}
}
- (BOOL)isRunning {
BOOL isRunning = NO;
@synchronized (self.runningOperations) {
isRunning = (self.runningOperations.count > 0);
}
return isRunning;
}
- (void)safelyRemoveOperationFromRunning:(nullable SDWebImageCombinedOperation*)operation {
@synchronized (self.runningOperations) {
if (operation) {
[self.runningOperations removeObject:operation];
}
}
}
為了防止self.runningOperations
被多線程操作,所有對self.runningOperations
的訪問都加上了@synchronized臨界區,@synchronized里的代碼段不會同時在多個線程中被執行。
SDWebImageManagerTests
NSString *workingImageURL = @"http://s3.amazonaws.com/fast-image-cache/demo-images/FICDDemoImage001.jpg";
(void)test02ThatDownloadInvokesCompletionBlockWithCorrectParamsAsync {
__block XCTestExpectation *expectation = [self expectationWithDescription:@"Image download completes"];
NSURL *originalImageURL = [NSURL URLWithString:workingImageURL];
[[SDWebImageManager sharedManager] loadImageWithURL:originalImageURL
options:SDWebImageRefreshCached
progress:nil
completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
expect(image).toNot.beNil();
expect(error).to.beNil();
expect(originalImageURL).to.equal(imageURL);
[expectation fulfill];
expectation = nil;
}];
expect([[SDWebImageManager sharedManager] isRunning]).to.equal(YES);
[self waitForExpectationsWithTimeout:kAsyncTestTimeout handler:nil];
}
SDWebImage
的單元測試依賴了網絡訪問,這個不太好。我們開發中需要做類似功能的單元測試時,可以考慮用OHHTTPStubs
來搞些假的網絡請求。
SDWebImage源碼系列
SDWebImage源碼之SDImageCache
SDWebImage源碼之SDWebImageDownloader
SDWebImage源碼之SDWebImageManager