SDWebImageソースのSDWebImageDownloader
10155 ワード
その名の通り、
この2つのクラスを組み合わせることは,後続の類似機構の開発の参考とすることができる非同期同時機構の設計モデルとなった.
以下のコードおよび解析は、041842 bf 085 cbb 711 f 0 d 9 e 099 e 6 acbf 6 fd 533 b 0 cというcommitに基づいている.
同じURLが1つのネットワーク要求のみを発行することを保証するために、
この方法は、マルチスレッドが
前述したように、複数の場所で同じURLの画像ダウンロードが同時にトリガーされる可能性があり、「SDWebImageDownloader」はダウンロード用の
画像のダウンロードが完了しても異常が発生しても、
テストするクラスにCategoryを追加することで、プライベートメソッドをインタフェースに言及する必要がなく、プライベートプロパティとメソッドをテストに暴露することができます.
SDWebImageソースコードのSDImageCache SDWebImageソースコードのSDWebImageDownloader SDWebImageソースコードのSDWebImageManager
SDWebImageDownloader
はSDWebImage
ライブラリで画像ダウンロードの管理を担当しています.ダウンロード画像用のNSURLSession
を維持していますが、直接ネットワークとは付き合いません.一連のSDWebImageDownloaderOperation
を管理しており、ダウンロードの実際の作業はSDWebImageDownloaderOperation
によって行われています.この2つのクラスを組み合わせることは,後続の類似機構の開発の参考とすることができる非同期同時機構の設計モデルとなった.
以下のコードおよび解析は、041842 bf 085 cbb 711 f 0 d 9 e 099 e 6 acbf 6 fd 533 b 0 cというcommitに基づいている.
SDWebImageDownloader & SDWebImageDownloaderOperation
@interface SDWebImageDownloader ()
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
// Identify the operation that runs this task and pass it the delegate method
SDWebImageDownloaderOperation *dataOperation = [self operationWithTask:dataTask];
[dataOperation URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
}
@end
---
@interface SDWebImageDownloaderOperation : NSOperation
// This is weak because it is injected by whoever manages this session. If this gets nil-ed out, we won't be able to run
// the task associated with this operation
@property (weak, nonatomic, nullable) NSURLSession *unownedSession;
// This is set if we're using not using an injected NSURLSession. We're responsible of invalidating this one
@property (strong, nonatomic, nullable) NSURLSession *ownedSession;
- (void)start {
...
NSURLSession *session = self.unownedSession;
if (!self.unownedSession) {
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
sessionConfig.timeoutIntervalForRequest = 15;
/**
* Create the session for this task
* We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
* method calls and completion handler calls.
*/
self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
delegate:self
delegateQueue:nil];
session = self.ownedSession;
}
self.dataTask = [session dataTaskWithRequest:self.request];
self.executing = YES;
}
[self.dataTask resume];
...
}
@end
SDWebImageDownloaderOperation
クラスは、外部からセッションを設定すること(複数のOperationsが1つのセッションを共有する)と、1つのセッションを自分で維持する2つのモードをサポートします.行動統一のため、SDWebImageDownloader
クラスはNSURLSessionTaskDelegate
とNSURLSessionTaskDelegate
の2つのプロトコルを実現したが、これらのプロトコルの具体的な実装はSDWebImageDownloaderOperation
に渡された.@interface SDWebImageDownloader ()
@property (strong, nonatomic, nonnull) NSMutableDictionary *URLOperations;
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)())createCallback {
...
dispatch_barrier_sync(self.barrierQueue, ^{
SDWebImageDownloaderOperation *operation = self.URLOperations[url];
if (!operation) {
operation = createCallback();
self.URLOperations[url] = operation;
__weak SDWebImageDownloaderOperation *woperation = operation;
operation.completionBlock = ^{
SDWebImageDownloaderOperation *soperation = woperation;
if (!soperation) return;
if (self.URLOperations[url] == soperation) {
[self.URLOperations removeObjectForKey:url];
};
};
}
});
...
}
- (void)cancel:(nullable SDWebImageDownloadToken *)token {
dispatch_barrier_async(self.barrierQueue, ^{
SDWebImageDownloaderOperation *operation = self.URLOperations[token.url];
BOOL canceled = [operation cancel:token.downloadOperationCancelToken];
if (canceled) {
[self.URLOperations removeObjectForKey:token.url];
}
});
}
@end
同じURLが1つのネットワーク要求のみを発行することを保証するために、
SDWebImageDownloader
は、URLをkeyとする辞書self.URLOperations
を維持し、すべてのSDWebImageDownloaderOperation
を保存する.この方法は、マルチスレッドが
self.URLOperations
を同時に動作する問題を防止するために、SDWebImageDownloader
がself.URLOperations
に対する専門的なキュー処理の動作を維持するために、任意のスレッドで呼び出される可能性がある.ここのコードをよく見ると、NSOperationのcompletionBlock
もself.URLOperations
を操作していますが、self.barrierQueue
には実行されていません.すでにこの問題に対してSDWebImage
にPull Requestを提出したネットユーザーがいる.@interface SDWebImageDownloader ()
@property (strong, nonatomic, nonnull) NSMutableDictionary *URLOperations;
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)())createCallback {
...
__block SDWebImageDownloadToken *token = nil;
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
token = [SDWebImageDownloadToken new];
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
return token;
}
@end
---
typedef NSMutableDictionary SDCallbacksDictionary;
@interface SDWebImageDownloaderOperation ()
@property (strong, nonatomic, nonnull) NSMutableArray *callbackBlocks;
- (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];
dispatch_barrier_async(self.barrierQueue, ^{
[self.callbackBlocks addObject:callbacks];
});
return callbacks;
}
- (BOOL)cancel:(nullable id)token {
__block BOOL shouldCancel = NO;
dispatch_barrier_sync(self.barrierQueue, ^{
[self.callbackBlocks removeObjectIdenticalTo:token];
if (self.callbackBlocks.count == 0) {
shouldCancel = YES;
}
});
if (shouldCancel) {
[self cancel];
}
return shouldCancel;
}
@end
前述したように、複数の場所で同じURLの画像ダウンロードが同時にトリガーされる可能性があり、「SDWebImageDownloader」はダウンロード用の
SDWebImageDownloaderOperation
オブジェクトを再利用している.しかし、この最適化は外部に対して透過的であり、SDWebImageDownloaderOperation
は、ダウンロードを呼び出す各オブジェクトがダウンロード完了のコールバックを正しく受信できることを保証しなければならない.SDWebImageDownloaderOperation
は、すべてのコールバックブロックを維持するためにcallbackBlocks
という配列を保存する.ダウンロード進捗更新とダウンロード完了のコールバックblockは合わせてSDCallbacksDictionary
としてcallbackBlocks
に保存される.また、このSDCallbacksDictionary
のポインタは、キャンセル要求用のフラグとして返される.SDWebImageDownloader
と同様に、SDWebImageDownloaderOperation
も動作self.barrierQueue
として1つのself.callbackBlocks
を維持する.#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
#endif
- (void)callCompletionBlocksWithImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
error:(nullable NSError *)error
finished:(BOOL)finished {
NSArray *completionBlocks = [self callbacksForKey:kCompletedCallbackKey];
dispatch_main_async_safe(^{
for (SDWebImageDownloaderCompletedBlock completedBlock in completionBlocks) {
completedBlock(image, imageData, error, finished);
}
});
}
画像のダウンロードが完了しても異常が発生しても、
completionBlock
はメインスレッド処理に投げ込まれます.SDWebImageDownloaderTests
/**
* Category for SDWebImageDownloader so we can access the operationClass
*/
@interface SDWebImageDownloader ()
@property (assign, nonatomic, nullable) Class operationClass;
@property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue;
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)())createCallback;
@end
テストするクラスにCategoryを追加することで、プライベートメソッドをインタフェースに言及する必要がなく、プライベートプロパティとメソッドをテストに暴露することができます.
SDWebImageソースシリーズ
SDWebImageソースコードのSDImageCache SDWebImageソースコードのSDWebImageDownloader SDWebImageソースコードのSDWebImageManager