preface

SDWebImage is an excellent image caching framework in ios, has experienced several versions, although the performance is not the highest, but from the point of view of security and maintenance, very suitable for most people to use – SDWebImage source code

Main Framework Introduction

The main logic is as follows

WebCache: classified information for UIView series, including call operation to cancel download, call ImageManager to obtain image content information, and processing operations after obtaining images

SDWebImageManager: A class for retrieving image content, acting as an intermediate management class to coordinate operations between cache fetching and network downloading

SDImageCache: Operation class for memory cache and disk cache. This class gets images from memory first and content from disk cache second

SDMemoryCache: Maintains the read operations of the cache

SDDiskCache: maintains and reads the disk cache

SDWebImageDownLoader: responsible for managing the image download operation, including to the SDWebImageDownloaderOperation network mapping operation request

SDWebImageDownloaderOperation: custom NSOperation subclass for more precise control of the process of images are downloaded, and network data in the process of actual receiving class for download, avoid kind of multiple data written to save problem, receive data after the callback

SDWebImage source code introduction

SDWebImage source code mainly introduces the above several classes, used to understand its core logic, in order to call the classification method as a benchmark, introduced the source code

Some tips will also be introduced for us to understand and use

WebCache classification

By categorizing the image load method calls, you end up in the following methods

- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                           context:(nullable SDWebImageContext *)context
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDImageLoaderProgressBlock)progressBlock
                         completed:(nullable SDInternalCompletionBlock)completedBlock {
    if (context) {
        // copy to avoid mutable object
        context = [context copy];
    } else {
        context = [NSDictionary dictionary];
    }
    NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
    // Save the key if there is no key
    if(! validOperationKey) { validOperationKey = NSStringFromClass([selfclass]);
        SDWebImageMutableContext *mutableContext = [context mutableCopy];
        mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
        context = [mutableContext copy];
    }
    self.sd_latestOperationKey = validOperationKey;
    
    // Cancel the operation subqueue corresponding to the key to avoid performance waste and error callback. The key is bound to the current View
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    self.sd_imageURL = url;
    
    if(! (options & SDWebImageDelayPlaceholder)) {// The placeholder image is not loaded lazily, and the placeholder image is loaded immediately
        // This is a synchronous queue, which is determined by the label of the queue, not by the thread
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil 
                basedOnClassOrViaCustomSetImageBlock:setImageBlock 
                cacheType:SDImageCacheTypeNone imageURL:url];
        });
    }
    
    if (url) {
    
        // This is about setting the Progress callback.// Use SDWebImageManager to load images
        @weakify(self);
        id <SDWebImageOperation> operation = [manager loadImageWithURL:url 
            options:options context:context progress:combinedProgressBlock 
                completed:^(UIImage *image, NSData *data, NSError *error, 
                     SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            @strongify(self);
            if(! self) {return; }
            // Mark progress completed
            // if the progress not been updated, mark it to complete state
            if(imageProgress && finished && ! error && imageProgress.totalUnitCount ==0 && imageProgress.completedUnitCount == 0) {
                imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
                imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
            }
            
#if SD_UIKIT || SD_MAC
            // check and stop image indicator
            if (finished) {
                [self sd_stopImageIndicator];
            }
#endif
            // Check whether callbacks and active setting of image are required according to optionsBOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage); BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) || (! image && ! (options & SDWebImageDelayPlaceholder)));// Set the callback
            SDWebImageNoParamsBlock callCompletedBlockClosure = ^{
                if(! self) {return; }
                if(! shouldNotSetImage) {// Update view content directly, as needed
                    [self sd_setNeedsLayout];
                }
                // Call back image and data
                if(completedBlock && shouldCallCompletedBlock) { completedBlock(image, data, error, cacheType, finished, url); }};if (shouldNotSetImage) {
                // No need to set the image automatically, then callback in the main queue
                dispatch_main_async_safe(callCompletedBlockClosure);
                return;
            }
            
            UIImage *targetImage = nil;
            NSData *targetData = nil;
            if (image) {
                targetImage = image;
                targetData = data;
            } else if (options & SDWebImageDelayPlaceholder) {
                // case 2b: we got no image and the SDWebImageDelayPlaceholder flag is settargetImage = placeholder; targetData = nil; }...// Depending on the type of picture, set the picture in the main thread and call block
            dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
                [self sd_setImage:targetImage imageData:targetData
                    basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition 
                        cacheType:cacheType imageURL:imageURL];
#else
                [self sd_setImage:targetImage imageData:targetData 
                    basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType 
                        imageURL:imageURL];
#endif
                callCompletedBlockClosure();
            });
        }];
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    } else{#if SD_UIKIT || SD_MAC
        [self sd_stopImageIndicator];
#endif
        dispatch_main_async_safe(^{
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain 
                    code:SDWebImageErrorInvalidURL 
                    userInfo: @ {NSLocalizedDescriptionKey : @"Image url is nil"}]; completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url); }}); }}Copy the code

dispatch_main_async_safe

The main queue is called by the label of the queue, rather than the main thread. Since the main queue is a serial queue, it is convenient to use the macro to call the block directly

#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
    if(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())) {\ block(); The \}else{\ dispatch_async(dispatch_get_main_queue(), block); \ } #endifCopy the code

SDWebImageManager image loading management class

It is an image loading management class, through calling SDWebImageCache and SDWebImageDownloader to achieve cache loading first, second network download

The following method is mainly to judge whether need shelf logic, finally get into callCacheProcessForOperation method

- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
                                          options:(SDWebImageOptions)options
                                          context:(nullable SDWebImageContext *)context
                                         progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                        completed: nonnull completedBlock {SDInternalCompletionBlock),...// The url failed
    if (url.absoluteString.length == 0| | (! (options & SDWebImageRetryFailed) && isFailedUrl)) { NSString *description = isFailedUrl ? @"Image url is blacklisted" : @"Image url is nil";
        NSInteger code = isFailedUrl ? SDWebImageErrorBlackListed : SDWebImageErrorInvalidURL;
        [self callCompletionBlockForOperation:operation 
            completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain 
                code:code userInfo:@{NSLocalizedDescriptionKey : description}] url:url];
        return operation;
    }

    SD_LOCK(_runningOperationsLock);
    [self.runningOperations addObject:operation];
    SD_UNLOCK(_runningOperationsLock);
    
    // Preprocess the options and context arg to decide the final the result for manager
    SDWebImageOptionsResult *result = [self processedResultForURL:url 
        options:options context:context];
    
    // Start the entry to load image from cache
    // Start getting images
    [self callCacheProcessForOperation:operation url:url 
        options:result.options context:result.context 
        progress:progressBlock completed:completedBlock];

    return operation;
}
Copy the code

Load the cached

The actual loading image, caching, and network request invocation logic are in the methods below

- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                 url:(nonnull NSURL *)url
                             options:(SDWebImageOptions)options
                             context:(nullable SDWebImageContext *)context
                            progress:(nullable SDImageLoaderProgressBlock)progressBlock
                           completed:(nullable SDInternalCompletionBlock)completedBlock {
    // Grab the image cache to use
    // Get the SDImageCache cache management class
    id<SDImageCache> imageCache;
    if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
        imageCache = context[SDWebImageContextImageCache];
    } else {
        imageCache = self.imageCache;
    }
    // Get the query cache type
    SDImageCacheType queryCacheType = SDImageCacheTypeAll;
    if (context[SDWebImageContextQueryCacheType]) {
        queryCacheType = [context[SDWebImageContextQueryCacheType] integerValue];
    }
    
    // Check whether we should query cache
    // Determine whether to fetch from the cacheBOOL shouldQueryCache = ! SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);if (shouldQueryCache) {
        // Get the key based on the URL
        NSString *key = [self cacheKeyForURL:url context:context];
        @weakify(operation);
        // Get the cache contents from the SDImageCache class by key, as described later
        operation.cacheOperation = [imageCache queryImageForKey:key 
            options:options context:context cacheType:queryCacheType 
            completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable 
                cachedData, SDImageCacheType cacheType) {
            @strongify(operation);
            // Operation does not exist or the task is cancelled
            if(! operation || operation.isCancelled) {// Image combined operation cancelled by user
                [self callCompletionBlockForOperation:operation completion:completedBlock 
                    error:[NSError errorWithDomain:SDWebImageErrorDomain 
                    code:SDWebImageErrorCancelled 
                    userInfo: @ {NSLocalizedDescriptionKey : 
                        @"Operation cancelled by user during querying the cache"}] 
                    url:url];
                [self safelyRemoveOperationFromRunning:operation];
                return;
            } else if(context[SDWebImageContextImageTransformer] && ! cachedImage) {// Query the original graph, instead of downloading, if the original graph query failed, then start downloading, because of the logic and method basic consistent, it is not used
                // Have a chance to query original cache instead of downloading
                [self callOriginalCacheProcessForOperation:operation 
                    url:url options:options context:context progress:progressBlock 
                    completed:completedBlock];
                return;
            }
            // Make a direct network request to download the image
            // Continue download process
            [self callDownloadProcessForOperation:operation url:url 
                options:options context:context cachedImage:cachedImage 
                cachedData:cachedData cacheType:cacheType progress:progressBlock 
                completed:completedBlock];
        }];
    } else {
        // Direct network request
        // Continue download process
        [self callDownloadProcessForOperation:operation url:url 
        options:options context:context cachedImage:nil cachedData:nil 
        cacheType:SDImageCacheTypeNone progress:progressBlock 
        completed:completedBlock]; }}Copy the code

Download images from the Internet

The call method for downloading an image from the network is shown below

// Download process
- (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                    url:(nonnull NSURL *)url
                                options:(SDWebImageOptions)options
                                context:(SDWebImageContext *)context
                            cachedImage:(nullable UIImage *)cachedImage
                             cachedData:(nullable NSData *)cachedData
                              cacheType:(SDImageCacheType)cacheType
                               progress:(nullable SDImageLoaderProgressBlock)progressBlock
                              completed:(nullable SDInternalCompletionBlock)completedBlock {
    // Grab the image loader to use
    // Get the proxy class that implements the ImageLoader protocol to call the download method
    id<SDImageLoader> imageLoader;
    if ([context[SDWebImageContextImageLoader] conformsToProtocol:@protocol(SDImageLoader)]) {
        imageLoader = context[SDWebImageContextImageLoader];
    } else {
        imageLoader = self.imageLoader;
    }
    
    // Check whether we should download image from networkBOOL shouldDownload = ! SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly); shouldDownload &= (! cachedImage || options & SDWebImageRefreshCached); shouldDownload &= (! [self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);if ([imageLoader respondsToSelector:@selector(canRequestImageForURL:options:context:)]) {
        shouldDownload &= [imageLoader canRequestImageForURL:url options:options context:context];
    } else {
        shouldDownload &= [imageLoader canRequestImageForURL:url];
    }
    // Start downloading images if needed
    if (shouldDownload) {
        if (cachedImage && options & SDWebImageRefreshCached) {
            [self callCompletionBlockForOperation:operation completion:completedBlock 
                image:cachedImage data:cachedData error:nil cacheType:cacheType 
                finished:YES url:url];
            SDWebImageMutableContext *mutableContext;
            if (context) {
                mutableContext = [context mutableCopy];
            } else {
                mutableContext = [NSMutableDictionary dictionary];
            }
            mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage;
            context = [mutableContext copy];
        }
        
        @weakify(operation);
        // Download the image method call
        operation.loaderOperation = [imageLoader requestImageWithURL:url 
            options:options context:context progress:progressBlock completed:^(UIImage 
                *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
            @strongify(operation);
            // Cancel download failure direct callback
            if(! operation || operation.isCancelled) { ... }else {
                // The download is successful
                // If the download succeeds, it will be removed from the failed list
                if ((options & SDWebImageRetryFailed)) {
                    SD_LOCK(self->_failedURLsLock);
                    [self.failedURLs removeObject:url];
                    SD_UNLOCK(self->_failedURLsLock);
                }
                // Continue store cache process
                // Cache processing, including rotation image recovery, disk cache, etc
                [self callStoreCacheProcessForOperation:operation url:url
                    options:options context:context downloadedImage:downloadedImage 
                    downloadedData:downloadedData finished:finished 
                    progress:progressBlock completed:completedBlock];
            }
            
            if(finished) { [self safelyRemoveOperationFromRunning:operation]; }}]; }else if (cachedImage) {
        // Find the cache image, end
        [self callCompletionBlockForOperation:operation completion:completedBlock 
            image:cachedImage data:cachedData error:nil cacheType:cacheType 
            finished:YES url:url];
        [self safelyRemoveOperationFromRunning:operation];
    } else {
        // The image is not cached
        // Image not in cache and download disallowed by delegate
        [self callCompletionBlockForOperation:operation completion:completedBlock 
            image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES 
            url:url]; [self safelyRemoveOperationFromRunning:operation]; }}Copy the code

SDImageCache Fast cache management class

The first method is mainly to judge the introduction of options, mainly the following method

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key 
    options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context 
    cacheType:(SDImageCacheType)queryCacheType 
    done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
    // There is no direct return null for key
    if(! key) {if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }
    // Invalid cache type No cache ends
    if (queryCacheType == SDImageCacheTypeNone) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }
    
    // First check the in-memory cache...
    // If not disk only, start fetching from cache memory
    UIImage *image;
    if(queryCacheType ! = SDImageCacheTypeDisk) { image = [self imageFromMemoryCacheForKey:key]; }// Get the image
    if (image) {
        // Process the image, including rotating the image (the image may be rotated)
        if (options & SDImageCacheDecodeFirstFrameOnly) {
            // Ensure static image
            Class animatedImageClass = image.class;
            if (image.sd_isAnimated || 
            ([animatedImageClass isSubclassOfClass:[UIImage class]] && 
                [animatedImageClass conformsToProtocol: @protocol(SDAnimatedImage))){#if SD_MAC
                image = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale 
                    orientation:kCGImagePropertyOrientationUp];
#else
                image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale 
                    orientation:image.imageOrientation];
#endif
            }
        } else if (options & SDImageCacheMatchAnimatedImageClass) {
            // Check image class matching
            Class animatedImageClass = image.class;
            Class desiredImageClass = context[SDWebImageContextAnimatedImageClass];
            if(desiredImageClass && ! [animatedImageClass isSubclassOfClass:desiredImageClass]) { image = nil; }}}// If only from the cache, endBOOL shouldQueryMemoryOnly = (queryCacheType == SDImageCacheTypeMemory) || (image && ! (options & SDImageCacheQueryMemoryData));if (shouldQueryMemoryOnly) {
        if (doneBlock) {
            doneBlock(image, nil, SDImageCacheTypeMemory);
        }
        return nil;
    }
    
    // If the cache is not found, start caching on disk
    // Second check the disk cache...
    NSOperation *operation = [NSOperation new];
    // Check whether we need to synchronously query disk
    // 1. in-memory cache hit & memoryDataSync
    // 2. in-memory cache miss & diskDataSyncBOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) || (! image && options & SDImageCacheQueryDiskDataSync));// Define blocks for IO calls, disk access, and memory writes
    void(^queryDiskBlock)(void) = ^ {if (operation.isCancelled) {
            if (doneBlock) {
                doneBlock(nil, nil, SDImageCacheTypeNone);
            }
            return;
        }
        
        @autoreleasepool {
            // Query image data from disk
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage;
            if (image) {
                // the image is from in-memory cache, but need image data
                // If an assignment is found in memory, it is returned directly
                diskImage = image;
            } else if (diskData) {
                // The data found on the disk is saved to the cache
                BOOL shouldCacheToMomery = YES;
                if (context[SDWebImageContextStoreCacheType]) {
                    SDImageCacheType cacheType = 
                        [context[SDWebImageContextStoreCacheType] integerValue];
                    shouldCacheToMomery = (cacheType == SDImageCacheTypeAll || 
                        cacheType == SDImageCacheTypeMemory);
                }
                // decode image data only if in-memory cache missed
                diskImage = [self diskImageForKey:key data:diskData 
                    options:options context:context];
                if (shouldCacheToMomery && diskImage && self.config.shouldCacheImagesInMemory) {
                    NSUInteger cost = diskImage.sd_memoryCost;
                    [self.memoryCache setObject:diskImage forKey:key cost:cost];
                }
            }
           
            if (doneBlock) {
                if (shouldQueryDiskSync) {
                    // Call back to the IO queue as needed
                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                } else {
                    // Call back the content in the main threaddispatch_async(dispatch_get_main_queue(), ^{ doneBlock(diskImage, diskData, SDImageCacheTypeDisk); }); }}}};// Query in ioQueue to keep IO-safe
    // Execute blocks in the IO queue
    if (shouldQueryDiskSync) {
        dispatch_sync(self.ioQueue, queryDiskBlock);
    } else {
        dispatch_async(self.ioQueue, queryDiskBlock);
    }
    
    return operation;
}
Copy the code

SDWebImageDownloader, SDWebImageDownloaderOperation

SDWebImageDownloader together with SDWebImageDownloaderOperation download function

The SDWebImageDownloader is responsible for queue setup and proxy callback operations for network requests

NSOperation SDWebImageDownloaderOperation inheritance, and agent SDWebImageDownloader network request operation, is responsible for downloading cancel operations such as the beginning of the request

SDWebImageDownloader

Create a new task and add it to the download queue

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                   context:(nullable SDWebImageContext *)context
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    ...
    // Create a new task and add it to the download queue
    if(! operation || operation.isFinished || operation.isCancelled) {// Set parameters such as request
        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); Set the callback after the completion of the queue, is used to remove information operation.com pletionBlock = ^ {@ strongify (self);if(! self) {return;
            }
            SD_LOCK(self->_operationsLock);
            [self.URLOperations removeObjectForKey:url];
            SD_UNLOCK(self->_operationsLock);
        };
        self.URLOperations[url] = operation;
        [self.downloadQueue addOperation:operation];
    } else {
        /
        @synchronized (operation) {
            downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
        }
        if(! operation.isExecuting) {if (options & SDWebImageDownloaderHighPriority) {
                operation.queuePriority = NSOperationQueuePriorityHigh;
            } else if (options & SDWebImageDownloaderLowPriority) {
                operation.queuePriority = NSOperationQueuePriorityLow;
            } else {
                operation.queuePriority = NSOperationQueuePriorityNormal;
            }
        }
    }
    SD_UNLOCK(_operationsLock);
    
    SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
    token.url = url;
    token.request = operation.request;
    token.downloadOperationCancelToken = downloadOperationCancelToken;
    
    return token;
}
Copy the code

NSURLSession Proxy operation

As can be seen below, the request takes out the subtask according to the saved queue, and the callback method is processed by the agent corresponding to the corresponding subtask, thus avoiding complex data storage and task allocation

Here introduces SDWebImageDownloader delegate is not only simple introduce to the agency callback SDWebImageDownloaderOperation

#pragma mark 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
    NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:dataTask];
    if ([dataOperation respondsToSelector:@selector(URLSession:dataTask:didReceiveResponse:completionHandler:)]) {
        [dataOperation URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler];
    } else {
        if(completionHandler) { completionHandler(NSURLSessionResponseAllow); }}} - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {

    // Identify the operation that runs this task and pass it the delegate method
    NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:dataTask];
    if([dataOperation respondsToSelector:@selector(URLSession:dataTask:didReceiveData:)]) { [dataOperation URLSession:session dataTask:dataTask didReceiveData:data]; }} - (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
 willCacheResponse:(NSCachedURLResponse *)proposedResponse
 completionHandler: (void (^)(NSCachedURLResponse *cachedResponse))completionHandler {

    // Identify the operation that runs this task and pass it the delegate method
    NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:dataTask];
    if ([dataOperation respondsToSelector:@selector(URLSession:dataTask:willCacheResponse:completionHandler:)]) {
        [dataOperation URLSession:session dataTask:dataTask willCacheResponse:proposedResponse completionHandler:completionHandler];
    } else {
        if (completionHandler) {
            completionHandler(proposedResponse);
        }
    }
}

#pragma mark NSURLSessionTaskDelegate

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    
    // Identify the operation that runs this task and pass it the delegate method
    NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:task];
    if([dataOperation respondsToSelector:@selector(URLSession:task:didCompleteWithError:)]) { [dataOperation URLSession:session task:task didCompleteWithError:error]; }} - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
    
    // Identify the operation that runs this task and pass it the delegate method
    NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:task];
    if([dataOperation respondsToSelector:@selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)])  { [dataOperation URLSession:session task:task willPerformHTTPRedirection:response newRequest:request completionHandler:completionHandler]; }else {
        if(completionHandler) { completionHandler(request); }}} - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {

    // Identify the operation that runs this task and pass it the delegate method
    NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:task];
    if ([dataOperation respondsToSelector:@selector(URLSession:task:didReceiveChallenge:completionHandler:)]) {
        [dataOperation URLSession:session task:task didReceiveChallenge:challenge completionHandler:completionHandler];
    } else {
        if(completionHandler) { completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); }}} - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)) {
    
    // Identify the operation that runs this task and pass it the delegate method
    NSOperation<SDWebImageDownloaderOperation> *dataOperation = [self operationWithTask:task];
    if([dataOperation respondsToSelector:@selector(URLSession:task:didFinishCollectingMetrics:)]) { [dataOperation URLSession:session task:task didFinishCollectingMetrics:metrics]; }}Copy the code

SDWebImageDownloaderOperation rewrite

Rewrote the start method to make better use of NSOperation

- (void)start {
    @synchronized (self) {
        // If the task has already been canceled, the task ends
        if (self.isCancelled) {
            if(! self.isFinished) self.finished = YES;// Operation cancelled by user before sending the request
            [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user before sending the request"}]];
            [self reset];
            return;
        }

#if SD_UIKIT
        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:^{
                [wself cancel];
            }];
        }
#endif
        // Start the downloaded operation
        NSURLSession *session = self.unownedSession;
        if(! session) { 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. */
            session = [NSURLSession sessionWithConfiguration:sessionConfig
                                                    delegate:self
                                               delegateQueue:nil];
            self.ownedSession = session;
        }
        
        if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
            // Grab the cached data for later check
            NSURLCache *URLCache = session.configuration.URLCache;
            if(! URLCache) { URLCache = [NSURLCache sharedURLCache]; } NSCachedURLResponse *cachedResponse;// NSURLCache's `cachedResponseForRequest:` is not thread-safe, see https://developer.apple.com/documentation/foundation/nsurlcache#2317483
            @synchronized (URLCache) {
                cachedResponse = [URLCache cachedResponseForRequest:self.request];
            }
            if(cachedResponse) { self.cachedData = cachedResponse.data; self.response = cachedResponse.response; }}if(! session.delegate) {// Session been invalid and has no delegate at all
            [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Session delegate is nil and invalid"}]];
            [self reset];
            return;
        }
        
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;
    }
    // Set task priority according to enumeration
    if (self.dataTask) {
        if (self.options & SDWebImageDownloaderHighPriority) {
            self.dataTask.priority = NSURLSessionTaskPriorityHigh;
            self.coderQueue.qualityOfService = NSQualityOfServiceUserInteractive;
        } else if (self.options & SDWebImageDownloaderLowPriority) {
            self.dataTask.priority = NSURLSessionTaskPriorityLow;
            self.coderQueue.qualityOfService = NSQualityOfServiceBackground;
        } else {
            self.dataTask.priority = NSURLSessionTaskPriorityDefault;
            self.coderQueue.qualityOfService = NSQualityOfServiceDefault;
        }
        // Start the task
        [self.dataTask resume];
        / / callback progress
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
        }
        __block typeof(self) strongSelf = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:strongSelf];
        });
    } else {
        // Task error, end
        if(! self.isFinished) self.finished = YES; [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]]. [self reset]; }}Copy the code

Overriding the cancel method for NSOperation

- (void)cancel { @synchronized (self) { [self cancelInternal]; }} - (void)cancelInternal {
    if (self.isFinished) return;
    [super cancel];
    
    __block typeof(self) strongSelf = self;
    dispatch_async(dispatch_get_main_queue(), ^{
        [[NSNotificationCenter defaultCenter] 
            postNotificationName:SDWebImageDownloadStopNotification object:strongSelf];
    });
    
    // Cancel the task operation
    if (self.dataTask) {
        [self.dataTask cancel];
        self.dataTask = nil;
    }
    // Adjust the execution state and completion state
    if (self.isExecuting || self.isFinished) {
        if (self.isExecuting) self.executing = NO;
        if(! self.isFinished) self.finished = YES; }// Operation cancelled by user during sending the request
    // End the callback
    [self callCompletionBlocksWithError:
        [NSError errorWithDomain:SDWebImageErrorDomain 
            code:SDWebImageErrorCancelled 
            userInfo: @ {NSLocalizedDescriptionKey : 
                @"Operation cancelled by user during sending the request"}]];

    [self reset];
}
Copy the code

As you can see, manual KVC operations have been added to ensure that attributes trigger KVC


- (void)setFinished:(BOOL)finished {
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];
}

- (void)setExecuting:(BOOL)executing {
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}
Copy the code

The last

SDWebImage download process I believe that through the introduction of the diagram can see, the author will be a function split into each module, through the management class coordination call, for example: SDWebImageManager is responsible for the management of the cache class and network request class call, SDImageCache is responsible for fast cache and disk cache processing

Download module, by overwriting NSOperation and mapping download tasks to subtasks, avoids managing data for multiple download tasks and other callbacks in a single class