美文网首页
SDWebImage源码之SDWebImageDownloade

SDWebImage源码之SDWebImageDownloade

作者: Cyyyyyyyy | 来源:发表于2017-05-26 18:23 被阅读181次

    顾名思义,SDWebImageDownloaderSDWebImage库中负责图片下载的管理工作。它维护着下载图片用的NSURLSession,但不直接与网络打交道。它管理着一系列的SDWebImageDownloaderOperation,下载的实际工作都由SDWebImageDownloaderOperation进行。

    这两个类合起来成为了一个异步并发机制的设计范本,可以作为后续开发类似机制的参考。


    以下代码和分析都基于041842bf085cbb711f0d9e099e6acbf6fd533b0c这个commit。

    SDWebImageDownloader & SDWebImageDownloaderOperation

    @interface SDWebImageDownloader () <NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
    - (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 <NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
    // 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类支持从外部设置session(多个Opeations共享一个session)和自己维护一个session两种模式。
    为了行为统一,SDWebImageDownloader类实现了NSURLSessionTaskDelegateNSURLSessionTaskDelegate这两个协议,但是把这些协议的具体实现都转交给了SDWebImageDownloaderOperation

    @interface SDWebImageDownloader () <NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
    @property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, SDWebImageDownloaderOperation *> *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只发出一个网络请求,SDWebImageDownloader维护了一个以URL为key的字典self.URLOperations保存所有的SDWebImageDownloaderOperation

    这个方法可能会在任何线程被调用,为了防止出现多线程同时操作self.URLOperations的问题,SDWebImageDownloader维护了一个专门的队列处理对self.URLOperations的操作。
    仔细看这里的代码会发现一个问题,NSOperation的completionBlock也对self.URLOperations进行了操作,但是它没有被放在self.barrierQueue里执行。已经有网友针对这个问题给SDWebImage提了Pull Request

    @interface SDWebImageDownloader () <NSURLSessionTaskDelegate, NSURLSessionDataDelegate>
    @property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, SDWebImageDownloaderOperation *> *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<NSString *, id> SDCallbacksDictionary;
    
    @interface SDWebImageDownloaderOperation ()
    @property (strong, nonatomic, nonnull) NSMutableArray<SDCallbacksDictionary *> *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。下载进度更新和下载完成的回调block被合起来以SDCallbacksDictionary的形式保存在callbackBlocks里。同时,这个SDCallbacksDictionary的指针还被返回作为取消请求用的标记。
    SDWebImageDownloader一样,SDWebImageDownloaderOperation也维护了一个self.barrierQueue用作操作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<id> *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

    相关文章

      网友评论

          本文标题:SDWebImage源码之SDWebImageDownloade

          本文链接:https://www.haomeiwen.com/subject/guvxfxtx.html