- (SDWebImageCombinedOperation *)loadImageWithURL:(nullableNSURL *)url options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nonnull SDInternalCompletionBlock)completedBlock { SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; operation.manager = self; ... // Start the entry to load image from cache, the longest steps are below // Steps without transformer: // 1. query image from cache, miss // 2. download data and image // 3. store image to cache // Steps with transformer: // 1. query transformed image from cache, miss // 2. query original image from cache, miss // 3. download data and image // 4. do transform in CPU // 5. store original image to cache // 6. store transformed image to cache [self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock]; }
// Download process - (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation url:(nonnullNSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context cachedImage:(nullableUIImage *)cachedImage cachedData:(nullableNSData *)cachedData cacheType:(SDImageCacheType)cacheType progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock { // Mark the cache operation end @synchronized (operation) { operation.cacheOperation = nil; } // Grab the image loader to use id<SDImageLoader> imageLoader = context[SDWebImageContextImageLoader]; if (!imageLoader) { imageLoader = self.imageLoader; } // Check whether we should download image from network BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly); ... if (shouldDownload) { if (cachedImage && options & SDWebImageRefreshCached) { // If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image // AND try to re-download it in order to let a chance to NSURLCache to refresh it from server. [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES queue:context[SDWebImageContextCallbackQueue] url:url]; // Pass the cached image to the image loader. The image loader should check whether the remote image is equal to the cached image. ... } @weakify(operation); operation.loaderOperation = [imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) { ... // Continue transform process [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData cacheType:SDImageCacheTypeNone finished:finished completed:completedBlock]; } if (finished) { [self safelyRemoveOperationFromRunning:operation]; } }]; } ... }
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullableNSURL *)url options:(SDWebImageDownloaderOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock { id downloadOperationCancelToken; // When different thumbnail size download with same url, we need to make sure each callback called with desired size id<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter]; NSString *cacheKey; if (cacheKeyFilter) { cacheKey = [cacheKeyFilter cacheKeyForURL:url]; } else { cacheKey = url.absoluteString; } SDImageCoderOptions *decodeOptions = SDGetDecodeOptionsFromContext(context, [self.class imageOptionsFromDownloaderOptions:options], cacheKey); SD_LOCK(_operationsLock); NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url]; // There is a case that the operation may be marked as finished or cancelled, but not been removed from `self.URLOperations`. BOOL shouldNotReuseOperation; if (operation) { @synchronized (operation) { shouldNotReuseOperation = operation.isFinished || operation.isCancelled; } } else { shouldNotReuseOperation = YES; } if (shouldNotReuseOperation) { operation = [self createDownloaderOperationWithUrl:url options:options context:context]; if (!operation) { SD_UNLOCK(_operationsLock); if (completedBlock) { NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Downloader operation is nil"}]; completedBlock(nil, nil, error, YES); } returnnil; } @weakify(self); operation.completionBlock = ^{ @strongify(self); if (!self) { return; } SD_LOCK(self->_operationsLock); [self.URLOperations removeObjectForKey:url]; SD_UNLOCK(self->_operationsLock); }; [self.URLOperations setObject:operation forKey:url]; // Add the handlers before submitting to operation queue, avoid the race condition that operation finished before setting handlers. downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:decodeOptions]; // Add operation to operation queue only after all configuration done according to Apple's doc. // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock. [self.downloadQueue addOperation:operation]; } else { // When we reuse the download operation to attach more callbacks, there may be thread safe issue because the getter of callbacks may in another queue (decoding queue or delegate queue) // So we lock the operation here, and in `SDWebImageDownloaderOperation`, we use `@synchonzied (self)`, to ensure the thread safe between these two classes. @synchronized (operation) { downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:decodeOptions]; } } SD_UNLOCK(_operationsLock); SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation]; token.url = url; token.request = operation.request; token.downloadOperationCancelToken = downloadOperationCancelToken; return token; }
/// A async block operation, success after you call `completer` (not like `NSBlockOperation` which is for sync block, success on return) @interfaceSDAsyncBlockOperation : NSOperation