iOS SDWebImageソースコード研究(三)

59134 ワード

先のSDWebImageソースコード解析(一)とSDWebImageソースコード解析(二)では,オープンソース非同期ピクチャダウンロードライブラリSDWebImageのキャッシュ部分を解析した.この記事では、SDWebImageのダウンロード部を解析します.
SDWebImageの非同期ダウンロードSDWebImageDownloadは、その単一のオブジェクトsharedDownloaderを利用して、画像のダウンロードプロセスをよく構成することができます.sharedDownloaderが構成できる部分:
  • ダウンロードオプション
  • HTTPヘッダ
  • 圧縮、ダウンロード順序、最大同時数、ダウンロードタイムアウト等
  • ダウンロードオプション
    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つの操作キューに配置します.これにより、画像の同時ダウンロードが可能になります.