iOS SDWebImageソースコード研究(三)
59134 ワード
先のSDWebImageソースコード解析(一)とSDWebImageソースコード解析(二)では,オープンソース非同期ピクチャダウンロードライブラリSDWebImageのキャッシュ部分を解析した.この記事では、SDWebImageのダウンロード部を解析します.
SDWebImageの非同期ダウンロードSDWebImageDownloadは、その単一のオブジェクトsharedDownloaderを利用して、画像のダウンロードプロセスをよく構成することができます.sharedDownloaderが構成できる部分:ダウンロードオプション HTTPヘッダ 圧縮、ダウンロード順序、最大同時数、ダウンロードタイムアウト等 ダウンロードオプション
これらのオプションは、主にダウンロードの優先度、キャッシュ、バックグラウンドタスク実行、クッキー処理、認証のいくつかの態様に関連していることがわかります.
HTTPヘッダ
requestを設定します.AllHTTPHeaderFieldsは、HTTPヘッダを指定し、HTTPヘッダには画像タイプを許容できる情報が含まれており、使用者は自分でHTTPヘッダ情報を追加または削除することもできる.
スレッドのセキュリティ
カスタムパラレルスケジューリングキューbarrierQueueを使用して、すべてのダウンロード操作のネットワーク応答シーケンス化タスクを処理します.
SDWebImageの非同期ダウンロードSDWebImageDownloadは、その単一のオブジェクトsharedDownloaderを利用して、画像のダウンロードプロセスをよく構成することができます.sharedDownloaderが構成できる部分:
typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) {
SDWebImageDownloaderLowPriority = 1 << 0,
SDWebImageDownloaderProgressiveDownload = 1 << 1,
/** * ,request NSURLCache。 , NSURLCache */
SDWebImageDownloaderUseNSURLCache = 1 << 2,
/** * NSURLCache , nil imageData block */
SDWebImageDownloaderIgnoreCachedResponse = 1 << 3,
/** * iOS 4+ , 。 。 , 。 */
SDWebImageDownloaderContinueInBackground = 1 << 4,
/** * NSMutableURLRequest.HTTPShouldHandleCookies = YES NSHTTPCookieStore cookie */
SDWebImageDownloaderHandleCookies = 1 << 5,
/** * SSL 。 */
SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6,
/** * */
SDWebImageDownloaderHighPriority = 1 << 7,
};
これらのオプションは、主にダウンロードの優先度、キャッシュ、バックグラウンドタスク実行、クッキー処理、認証のいくつかの態様に関連していることがわかります.
HTTPヘッダ
@property (strong, nonatomic) NSMutableDictionary *HTTPHeaders;
#ifdef SD_WEBP
_HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];
#else
_HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];
#endif
- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field {
if (value) {
self.HTTPHeaders[field] = value;
}
else {
[self.HTTPHeaders removeObjectForKey:field];
}
}
- (NSString *)valueForHTTPHeaderField:(NSString *)field {
return self.HTTPHeaders[field];
}
requestを設定します.AllHTTPHeaderFieldsは、HTTPヘッダを指定し、HTTPヘッダには画像タイプを許容できる情報が含まれており、使用者は自分でHTTPヘッダ情報を追加または削除することもできる.
スレッドのセキュリティ
カスタムパラレルスケジューリングキューbarrierQueueを使用して、すべてのダウンロード操作のネットワーク応答シーケンス化タスクを処理します.
// This queue is used to serialize the handling of the network responses of all the download operation in a single queue
@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t barrierQueue;
スレッドのセキュリティを保証するために、すべての変更コールバックセットURLCallbacksの操作はdispatch_を使用します.barrier_syncはキューbarrierQueueに格納され、URLCallbakcsをクエリーする操作はdispatch_を使用するだけです.syncはキューbarrierQueueに入れます.// URLCallbacks
dispatch_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
});
// URLCallbacks
dispatch_barrier_sync(self.barrierQueue, ^{
BOOL first = NO;
if (!self.URLCallbacks[url]) {
self.URLCallbacks[url] = [NSMutableArray new];
first = YES;
}
NSMutableArray *callbacksForURL = self.URLCallbacks[url];
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
self.URLCallbacks[url] = callbacksForURL;
if (first) {
createCallback();
}
});
コールバック
各ピクチャのダウンロードは、ダウンロードの進捗コールバック、ダウンロードの完了コールバックなど、いくつかのコールバック操作に対応します.これらのコールバック操作はblock形式で表示され、以下に示します.//
typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
//
typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
//headers
typedef NSDictionary *(^SDWebImageDownloaderHeadersFilterBlock)(NSURL *url, NSDictionary *headers);
画像ダウンロードのこれらのコールバック情報は、SDWebImageDownloaderクラスのURLCallbacks属性に格納され、この属性は辞書であり、keyは画像のURLアドレスであり、valueは1つの配列であり、各画像の複数のコールバック情報を含む.
ダウンロード
ダウンロードリクエストの管理は、ダウンロードリクエスト全体をdownloadImageWithURL:options:progress:completed:メソッドに格納して処理します.- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
// block __block
__block SDWebImageDownloaderOperation *operation;
// retain cycle
__weak __typeof(self)wself = self;
// , block , downloadQueue 。
[self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{
NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// , options
// (NSURLCache+SDImageCache), , 。
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
if (wself.headersFilter) {
request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = wself.HTTPHeaders;
}
// SDWebImageDownloaderOperation ,
operation = [[wself.operationClass alloc] initWithRequest:request
options:options
//
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
});
for (NSDictionary *callbacks in callbacksForURL) {
dispatch_async(dispatch_get_main_queue(), ^{
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
if (callback) callback(receivedSize, expectedSize);
});
}
}
//
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_barrier_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
if (finished) {
[sself.URLCallbacks removeObjectForKey:url];
}
});
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
if (callback) callback(image, data, error, finished);
}
}
// URL URLCallbacks
cancelled:^{
SDWebImageDownloader *sself = wself;
if (!sself) return;
dispatch_barrier_async(sself.barrierQueue, ^{
[sself.URLCallbacks removeObjectForKey:url];
});
}];
operation.shouldDecompressImages = wself.shouldDecompressImages;
// operation
if (wself.username && wself.password) {
operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
}
// operation
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
// downloadQueue
[wself.downloadQueue addOperation:operation];
// LIFO , , 。
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
}];
return operation;
}
上記のメソッドはaddProgressCallback:andCompletedBlock:forURL:createCallback:メソッドを呼び出して要求された情報をダウンロード器に格納する- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
// URL URLCallbacks , 。 , block, 。
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return;
}
// dispatch_barrier_synv URLCallbacks 。
dispatch_barrier_sync(self.barrierQueue, ^{
BOOL first = NO;
if (!self.URLCallbacks[url]) {
self.URLCallbacks[url] = [NSMutableArray new];
first = YES;
}
// URL
NSMutableArray *callbacksForURL = self.URLCallbacks[url];
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
self.URLCallbacks[url] = callbacksForURL;
if (first) {
createCallback();
}
});
}
ダウンロード操作
各画像のダウンロードはOperation操作です.この操作の作成と操作キューへの参加の過程を上で分析した.次に、単一の操作の具体的な実装を見てみましょう.
SDWebImageは、画像ダウンロード操作の基本プロトコルとしてSDWebImageOperationを定義するプロトコルである.操作をキャンセルするためのcancelメソッドは1つだけ宣言されています.契約の具体的な声明は以下の通りです.@protocol SDWebImageOperation <NSObject>
- (void)cancel;
@end
SDWebImageは、NSOperationから継承され、SDWebImageDownloaderOperationプロトコルを採用したOperationクラスをカスタマイズします.継承されたメソッドに加えて、クラスは、上述した初期化メソッドinitWithRequest:options:pregress:completed:cancelled:.
画像のダウンロードについて、SDWebImageDownloaderOperationはURLロードシステムのNSURLConnectionクラス(iOS 7以降のNSURLSessionクラスは使用されていない)に完全に依存している.まず,SDWebImageDownloaderOperationクラスにおけるピクチャ実データのダウンロード処理,すなわちNSURLConnectionの各エージェント手法の実装を分析する.
まず、S D W e b I n g e DownloaderOperationは、ExtentionでNSURLConnectionDataDelegateプロトコルを採用し、プロトコルの以下のいくつかの方法を実現しました.connection:didReceiveResponse:
connection:didReceiveData:
connectionDidFinishLoading:
connection:didFailWithError:
connection:willCacheResponse:
connectionShouldUseCredentialStorage:
connection:willSendRequestForAuthenticationChallenge:
これらの方法は逐一分析せずにconnection:didReceiveResponse:とconnection:didReceiveData:の2つの方法を終点で分析します.
connection:didReceiveResponse法はNSURLResponseの実際のタイプとステータスコードを判断することにより,304を除く400以内のステータスコードに反応する.
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
//'304 Not Modified'
if (![response respondsToSelector:@selector(statusCode)] || ([((NSHTTPURLResponse *)response) statusCode] < 400 && [((NSHTTPURLResponse *)response) statusCode] != 304)) {
NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0;
self.expectedSize = expected;
if (self.progressBlock) {
self.progressBlock(0, expected);
}
self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
self.response = response;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self];
});
}
else {
NSUInteger code = [((NSHTTPURLResponse *)response) statusCode];
// '304 Not Modified' , remote 。
// 304 operation
if (code == 304) {
[self cancelInternal];
} else {
[self.connection cancel];
}
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
});
if (self.completedBlock) {
self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:[((NSHTTPURLResponse *)response) statusCode] userInfo:nil], YES);
}
CFRunLoopStop(CFRunLoopGetCurrent());
[self done];
}
}
connection:didReceiveData:メソッドの主なタスクは、データを受け入れることです.データを受信するたびに、既存のデータでCGImageSourceRefオブジェクトが作成され、処理されます.初めてデータを取得した場合(width+height=0)は、これらの画像情報を含むデータから画像の長さ、幅、方向などの情報を取り出して使用に備える.その後、画像のダウンロードが完了する前に、CGImageSourceRefオブジェクトを使用して画像オブジェクトを作成し、スケール、解凍操作を経てUIImageオブジェクトを生成してコールバックを完了します.もちろん,この方法で処理する必要があるのは進捗情報である.進捗コールバックを設定している場合は、現在のピクチャのダウンロード進捗を処理するために進捗コールバックを呼び出します.- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
[self.imageData appendData:data];
if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
// http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/
// @Nyx0uf
//
const NSInteger totalSize = self.imageData.length;
// , ,
CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
// , 、 、
if (width + height == 0) {
CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
if (properties) {
NSInteger orientationValue = -1;
CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
if (val) CFNumberGetValue(val, kCFNumberLongType, &height);
val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
if (val) CFNumberGetValue(val, kCFNumberLongType, &width);
val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);
CFRelease(properties);
// Core Graphics , , initWithCGIImage
// 。( connectionDidFinishLoading initWithData )
//
orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
}
}
//
if (width + height > 0 && totalSize < self.expectedSize) {
// , ,
CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
#ifdef TARGET_OS_IPHONE
// iOS 。
if (partialImageRef) {
const size_t partialHeight = CGImageGetHeight(partialImageRef);
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
CGColorSpaceRelease(colorSpace);
if (bmContext) {
CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef);
CGImageRelease(partialImageRef);
partialImageRef = CGBitmapContextCreateImage(bmContext);
CGContextRelease(bmContext);
}
else {
CGImageRelease(partialImageRef);
partialImageRef = nil;
}
}
#endif
// 、
if (partialImageRef) {
UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
UIImage *scaledImage = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:scaledImage];
}
else {
image = scaledImage;
}
CGImageRelease(partialImageRef);
dispatch_main_sync_safe(^{
if (self.completedBlock) {
self.completedBlock(image, nil, nil, NO);
}
});
}
}
CFRelease(imageSource);
}
if (self.progressBlock) {
self.progressBlock(self.imageData.length, self.expectedSize);
}
}
SDWebImageDownloaderOperationクラスはNSOperationクラスから継承されていると前述しました.簡単なmainメソッドではなく、より柔軟なstartメソッドを採用し、ダウンロードのステータスを自分で管理します.
startメソッドでは,我々が使用するNSURLConnectionオブジェクトをダウンロードし,画像のダウンロードを開始するとともに,ダウンロード開始の通知を投げ出す.startメソッドの具体的な実装は以下の通りである.- (void)start {
@synchronized (self) {
// , , YES
if (self.isCancelled) {
self.finished = YES;
[self reset];
return;
}
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
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:^{
__strong __typeof (wself) sself = wself;
if (sself) {
[sself cancel];
[app endBackgroundTask:sself.backgroundTaskId];
sself.backgroundTaskId = UIBackgroundTaskInvalid;
}
}];
}
#endif
self.executing = YES;
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
self.thread = [NSThread currentThread];
}
[self.connection start];
if (self.connection) {
if (self.progressBlock) {
self.progressBlock(0, NSURLResponseUnknownLength);
}
//
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
});
// runloop, 。
// Fail、Finish cancel CFRunloopStop(CFRunloopGetCurrent()) runloop。 // CFRunloopRun(), NSURLConnection , runloop, 。 // runloop , 。
if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
}
else {
CFRunLoopRun();
}
if (!self.isFinished) {
[self.connection cancel];
[self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
}
}
else {
if (self.completedBlock) {
self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
}
}
#if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
Class UIApplicationClass = NSClassFromString(@"UIApplication");
if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
return;
}
if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
[app endBackgroundTask:self.backgroundTaskId];
self.backgroundTaskId = UIBackgroundTaskInvalid;
}
#endif
}
もちろん、ダウンロードが完了したり、ダウンロードに失敗したりした後、現在のスレッドのrunloopを停止し、接続をクリアし、ダウンロード停止の通知を投げ出す必要があります.ダウンロードに成功すると、完全な画像データが処理され、適切なスケールと解凍操作が行われ、コールバックの完了に提供されます.具体的には-connectionDidFinishLoading:と-connection:didFailWithError:の実装を参照してください.
小結
ダウンロードの核心は、NSURLConnectionオブジェクトを利用してデータをロードすることです.各ピクチャのダウンロードは、1つの操作で完了し、これらの操作を1つの操作キューに配置します.これにより、画像の同時ダウンロードが可能になります.