SDWebImageソースのSDWebImageDownloader

10155 ワード

その名の通り、SDWebImageDownloaderSDWebImageライブラリで画像ダウンロードの管理を担当しています.ダウンロード画像用の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クラスはNSURLSessionTaskDelegateNSURLSessionTaskDelegateの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を同時に動作する問題を防止するために、SDWebImageDownloaderself.URLOperationsに対する専門的なキュー処理の動作を維持するために、任意のスレッドで呼び出される可能性がある.ここのコードをよく見ると、NSOperationのcompletionBlockself.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