美文网首页收藏ios
SDWebImage源码解析(一)——如何处理cell重用

SDWebImage源码解析(一)——如何处理cell重用

作者: XS_3a07 | 来源:发表于2019-07-21 15:43 被阅读0次

    首先我们从常用方法入手

    [self.imageview sd_setImageWithURL:[NSURL URLWithString:@"https://img.haomeiwen.com/i1552225/abe931d3223ca900.jpg?"]
                          placeholderImage:[UIImage imageNamed:@"SDWebImage_logo_small"]];
    

    跳过几个分离的接口后,我们进入sd_internalSetImageWithURL方法

    
    - (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 {
        context = [context copy]; // copy to avoid mutable object
        NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
        if (!validOperationKey) {
            validOperationKey = NSStringFromClass([self class]);
        }
        self.sd_latestOperationKey = validOperationKey;
        [self sd_cancelImageLoadOperationWithKey:validOperationKey];
        self.sd_imageURL = url;
        
        if (!(options & SDWebImageDelayPlaceholder)) {
            dispatch_main_async_safe(^{
                [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
            });
        }
        
        if (url) {
            // reset the progress
            NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
            if (imageProgress) {
                imageProgress.totalUnitCount = 0;
                imageProgress.completedUnitCount = 0;
            }
            
    #if SD_UIKIT || SD_MAC
            // check and start image indicator
            [self sd_startImageIndicator];
            id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
    #endif
            
            SDWebImageManager *manager = context[SDWebImageContextCustomManager];
            if (!manager) {
                manager = [SDWebImageManager sharedManager];
            }
            
            SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
                if (imageProgress) {
                    imageProgress.totalUnitCount = expectedSize;
                    imageProgress.completedUnitCount = receivedSize;
                }
    #if SD_UIKIT || SD_MAC
                if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
                    double progress = 0;
                    if (expectedSize != 0) {
                        progress = (double)receivedSize / expectedSize;
                    }
                    progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0
                    dispatch_async(dispatch_get_main_queue(), ^{
                        [imageIndicator updateIndicatorProgress:progress];
                    });
                }
    #endif
                if (progressBlock) {
                    progressBlock(receivedSize, expectedSize, targetURL);
                }
            };
            @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; }
                // 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
                
                BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
                BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
                                          (!image && !(options & SDWebImageDelayPlaceholder)));
                SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
                    if (!self) { return; }
                    if (!shouldNotSetImage) {
                        [self sd_setNeedsLayout];
                    }
                    if (completedBlock && shouldCallCompletedBlock) {
                        completedBlock(image, data, error, cacheType, finished, url);
                    }
                };
                
                // case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set
                // OR
                // case 1b: we got no image and the SDWebImageDelayPlaceholder is not set
                if (shouldNotSetImage) {
                    dispatch_main_async_safe(callCompletedBlockClojure);
                    return;
                }
                
                UIImage *targetImage = nil;
                NSData *targetData = nil;
                if (image) {
                    // case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
                    targetImage = image;
                    targetData = data;
                } else if (options & SDWebImageDelayPlaceholder) {
                    // case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
                    targetImage = placeholder;
                    targetData = nil;
                }
                
    #if SD_UIKIT || SD_MAC
                // check whether we should use the image transition
                SDWebImageTransition *transition = nil;
                if (finished && (options & SDWebImageForceTransition || cacheType == SDImageCacheTypeNone)) {
                    transition = self.sd_imageTransition;
                }
    #endif
                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
                    callCompletedBlockClojure();
                });
            }];
            [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);
                }
            });
        }
    }
    
    
    

    首先我们看到 [self sd_cancelImageLoadOperationWithKey:validOperationKey], 这句代码是干什么的先不管,继续往下看

    创建 SDWebImageManager

    SDWebImageManager *manager = context[SDWebImageContextCustomManager];
     if (!manager) {
         manager = [SDWebImageManager sharedManager];
     }
    
    SDWebImageManager.png

    进入sharedManager,里初始化了下面这些东西

    + (nonnull instancetype)sharedManager {
        static dispatch_once_t once;
        static id instance;
        dispatch_once(&once, ^{
            instance = [self new];
        });
        return instance;
    }
    
    - (nonnull instancetype)init {
        id<SDImageCache> cache = [[self class] defaultImageCache];
        if (!cache) {
            cache = [SDImageCache sharedImageCache];
        }
        id<SDImageLoader> loader = [[self class] defaultImageLoader];
        if (!loader) {
            loader = [SDWebImageDownloader sharedDownloader];
        }
        return [self initWithCache:cache loader:loader];
    }
    
    - (nonnull instancetype)initWithCache:(nonnull id<SDImageCache>)cache loader:(nonnull id<SDImageLoader>)loader {
        if ((self = [super init])) {
            _imageCache = cache;
            _imageLoader = loader;
            _failedURLs = [NSMutableSet new];
            _failedURLsLock = dispatch_semaphore_create(1);
            _runningOperations = [NSMutableSet new];
            _runningOperationsLock = dispatch_semaphore_create(1);
        }
        return self;
    }
    

    继续点击进入sharedDownloader(sharedImageCache就不在这里展示了,就先看看sharedDownloader),在initWithConfig方法里创建了_downloadQueue、_session、_URLOperations等,这就是下载图片的队列和session,队列的maxConcurrentOperationCount是6

    + (nonnull instancetype)sharedDownloader {
        static dispatch_once_t once;
        static id instance;
        dispatch_once(&once, ^{
            instance = [self new];
        });
        return instance;
    }
    
    - (nonnull instancetype)init {
        return [self initWithConfig:SDWebImageDownloaderConfig.defaultDownloaderConfig];
    }
    
    - (instancetype)initWithConfig:(SDWebImageDownloaderConfig *)config {
        self = [super init];
        if (self) {
            if (!config) {
                config = SDWebImageDownloaderConfig.defaultDownloaderConfig;
            }
            _config = [config copy];
            [_config addObserver:self forKeyPath:NSStringFromSelector(@selector(maxConcurrentDownloads)) options:0 context:SDWebImageDownloaderContext];
            _downloadQueue = [NSOperationQueue new];
            _downloadQueue.maxConcurrentOperationCount = _config.maxConcurrentDownloads;
            _downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
            _URLOperations = [NSMutableDictionary new];
    
    
          //中间代码省略...
    
    
            NSURLSessionConfiguration *sessionConfiguration = _config.sessionConfiguration;
            if (!sessionConfiguration) {
                sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
            }
            _session = [NSURLSession sessionWithConfiguration:sessionConfiguration
                                                     delegate:self
                                                delegateQueue:nil];
        }
        return self;
    }
    

    回到 sd_internalSetImageWithURL方法继续往下看,创建完manager,到

    @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);
    
          //中间代码省略...,得到的image就是我们要展示的
    
    
     }];
     [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    

    manager调用loadImageWithURL反回了一个operation <SDWebImageOperation>,然后进入sd_setImageLoadOperation方法,我们看到operation被保存了在一个通过runtime对象的关联给imageView的一个属性的NSMapTable类型的对象里,也就是相当于一个字典。operations的对value弱引用,当operation被释放的时候就会安全从operations移除。

    - (SDOperationsDictionary *)sd_operationDictionary {
        @synchronized(self) {
            SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
            if (operations) {
                return operations;
            }
            operations = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
            objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
            return operations;
        }
    }
    

    对应前面的sd_cancelImageLoadOperationWithKey方法,我们应该想到,当cell重用时,首先通过sd_cancelImageLoadOperationWithKey方法取消之前imageView保存的operation(如果能取到的话),然后设置新的operation。

    继续进入获取图片的loadImageWithURL方法

    - (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
                                              options:(SDWebImageOptions)options
                                              context:(nullable SDWebImageContext *)context
                                             progress:(nullable SDImageLoaderProgressBlock)progressBlock
                                            completed:(nonnull SDInternalCompletionBlock)completedBlock {
        // Invoking this method without a completedBlock is pointless
        NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
    
        // Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won't
        // throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
        if ([url isKindOfClass:NSString.class]) {
            url = [NSURL URLWithString:(NSString *)url];
        }
    
        // Prevents app crashing on argument type error like sending NSNull instead of NSURL
        if (![url isKindOfClass:NSURL.class]) {
            url = nil;
        }
    
        SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
        operation.manager = self;
    
        BOOL isFailedUrl = NO;
        if (url) {
            SD_LOCK(self.failedURLsLock);
            isFailedUrl = [self.failedURLs containsObject:url];
            SD_UNLOCK(self.failedURLsLock);
        }
    
        if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
            [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}] url:url];
            return operation;
        }
    
        SD_LOCK(self.runningOperationsLock);
        [self.runningOperations addObject:operation];
        SD_UNLOCK(self.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
        [self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock];
    
        return operation;
    }
    
    

    新建了一个operation< SDWebImageCombinedOperation >,返回给上一层,那么之前的operation<SDWebImageOperation>实际上就是这个operation,sd_cancelImageLoadOperationWithKey就是来取消这个operation的。

    然后operation被添加到runningOperations<NSMutableSet>,runningOperations归属于上面我们创建的manager,那么这个runningOperations就是管理operation< SDWebImageCombinedOperation >了。有add对应的肯定有remove了,在哪后面一定会看到。

    SD_LOCK(self.runningOperationsLock);
    [self.runningOperations addObject:operation];
    SD_UNLOCK(self.runningOperationsLock);
    

    然后进入callCacheProcessForOperation方法,判断要不要从缓存里获取(本文主要分析下载图片的部分,缓存这个部分基本上一笔带过)

    - (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
                                     url:(nonnull NSURL *)url
                                 options:(SDWebImageOptions)options
                                 context:(nullable SDWebImageContext *)context
                                progress:(nullable SDImageLoaderProgressBlock)progressBlock
                               completed:(nullable SDInternalCompletionBlock)completedBlock {
        // Check whether we should query cache
        BOOL shouldQueryCache = (options & SDWebImageFromLoaderOnly) == 0;
        if (shouldQueryCache) {
            id<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
            NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter];
            @weakify(operation);
            operation.cacheOperation = [self.imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
                @strongify(operation);
                if (!operation || operation.isCancelled) {
                    [self safelyRemoveOperationFromRunning:operation];
                    return;
                }
                // Continue download process
                [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
            }];
        } else {
            // Continue download process
            [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
        }
    }
    

    如果需要,queryImageForKey方法确认缓存里有没有这张图片,返回了一个新的operation<NSOperation>,赋值给operation.cacheOperation(operation.cacheOperation的operation是上面传进来的,也就是sd_cancelImageLoadOperationWithKey取消的那个)。回调block里我们看到了if (!operation || operation.isCancelled)这个判断,也就是回调block还没被执行时,cell重用了,operation.cacheOperation的operation被sd_cancelImageLoadOperationWithKey给取消了,回调block再执行时operation不存在,或已经被取消。然后被safelyRemoveOperationFromRunning方法从runningOperations里移除(对应上面的添加),直接return,上一个SDWebImage的流程结束了。

    如果没有cell重用,我们继续往下走,进入callDownloadProcessForOperation方法

    - (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 {
        // Check whether we should download image from network
        BOOL shouldDownload = (options & SDWebImageFromCacheOnly) == 0;
        shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached);
        shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
        shouldDownload &= [self.imageLoader canRequestImageForURL:url];
        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 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.
                SDWebImageMutableContext *mutableContext;
                if (context) {
                    mutableContext = [context mutableCopy];
                } else {
                    mutableContext = [NSMutableDictionary dictionary];
                }
                mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage;
                context = [mutableContext copy];
            }
            
            // `SDWebImageCombinedOperation` -> `SDWebImageDownloadToken` -> `downloadOperationCancelToken`, which is a `SDCallbacksDictionary` and retain the completed block below, so we need weak-strong again to avoid retain cycle
            @weakify(operation);
            operation.loaderOperation = [self.imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                @strongify(operation);
                if (!operation || operation.isCancelled) {
                    // Do nothing if the operation was cancelled
                    // See #699 for more details
                    // if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data
                } else if (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) {
                    // Image refresh hit the NSURLCache cache, do not call the completion block
                } else if (error) {
                    [self callCompletionBlockForOperation:operation completion:completedBlock error:error url:url];
                    BOOL shouldBlockFailedURL = [self shouldBlockFailedURLWithURL:url error:error];
                    
                    if (shouldBlockFailedURL) {
                        SD_LOCK(self.failedURLsLock);
                        [self.failedURLs addObject:url];
                        SD_UNLOCK(self.failedURLsLock);
                    }
                } else {
                    if ((options & SDWebImageRetryFailed)) {
                        SD_LOCK(self.failedURLsLock);
                        [self.failedURLs removeObject:url];
                        SD_UNLOCK(self.failedURLsLock);
                    }
                    
                    [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) {
            [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        } else {
            // 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];
        }
    }
    

    判断需不需要下载,不需要下载,执行传进来的completedBlock,图片加载成功。operation.cacheOperation的operation被,safelyRemoveOperationFromRunning方法从runningOperations里移除,SDWebImage的流程结束了。

    如果需要下载,requestImageWithURL方法返回一个operation<NSOperation<SDWebImageDownloaderOperation>>,赋值给operation.loaderOperation,判断 if (!operation || operation.isCancelled) 。回调block还没被执行时,cell重用了,peration.loaderOperation的operation被sd_cancelImageLoadOperationWithKey给取消了,回调block再执行时operation不存在,或已经被取消。然后被safelyRemoveOperationFromRunning方法从runningOperations里移除,上一个SDWebImage的流程结束了。

    如果没有cell重用,成功下载后,执行block内代码,进入callStoreCacheProcessForOperation方法处理存储展示图片。peration.loaderOperation的operation被safelyRemoveOperationFromRunning方法从runningOperations里移除,SDWebImage的流程结束了。

    继续进入下载图片的requestImageWithURL方法,最终进到downloadImageWithURL方法,

    - (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                       options:(SDWebImageDownloaderOptions)options
                                                       context:(nullable SDWebImageContext *)context
                                                      progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                     completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
        // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
        if (url == nil) {
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}];
                completedBlock(nil, nil, error, YES);
            }
            return nil;
        }
        
        SD_LOCK(self.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`.
        if (!operation || operation.isFinished || operation.isCancelled) {
            operation = [self createDownloaderOperationWithUrl:url options:options context:context];
            if (!operation) {
                SD_UNLOCK(self.operationsLock);
                if (completedBlock) {
                    NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Downloader operation is nil"}];
                    completedBlock(nil, nil, error, YES);
                }
                return nil;
            }
            @weakify(self);
            operation.completionBlock = ^{
                @strongify(self);
                if (!self) {
                    return;
                }
                SD_LOCK(self.operationsLock);
                [self.URLOperations removeObjectForKey:url];
                SD_UNLOCK(self.operationsLock);
            };
            self.URLOperations[url] = operation;
            // 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 if (!operation.isExecuting) {
            if (options & SDWebImageDownloaderHighPriority) {
                operation.queuePriority = NSOperationQueuePriorityHigh;
            } else if (options & SDWebImageDownloaderLowPriority) {
                operation.queuePriority = NSOperationQueuePriorityLow;
            } else {
                operation.queuePriority = NSOperationQueuePriorityNormal;
            }
        }
        SD_UNLOCK(self.operationsLock);
        
        id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
        
        SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
        token.url = url;
        token.request = operation.request;
        token.downloadOperationCancelToken = downloadOperationCancelToken;
        token.downloader = self;
        
        return token;
    }
    

    这里我们又看到一个新的operation<SDWebImageDownloaderOperation>

    NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
    

    这个operation是被self.URLOperations字典管理的,self.URLOperations是属于manager的imageLoader下载器的,imageLoader是属于manager的,也就是说不管有多少个cell正在加载图片,manager只有一个,imageLoader只有一个,那么self.URLOperations也只有一个。
    如果能够从self.URLOperations取到operation,说明这个cell是复用的

    operation.completionBlock = ^{
                @strongify(self);
                if (!self) {
                    return;
                }
                SD_LOCK(self.operationsLock);
                [self.URLOperations removeObjectForKey:url];
                SD_UNLOCK(self.operationsLock);
     };
    

    设置operation任务完时成从URLOperations移除

    self.URLOperations[url] = operation;
     [self.downloadQueue addOperation:operation];
    

    operation被添加到self.downloadQueue时就开始自动start了,但是我们还没有看到NSURLSessionTask什么创建的,怎么就开始下载了?所以我们应该想到可能是operation重写了start方法,NSURLSessionTask在start方法里创建,并且resume, 我们先不管。继续往下看这个方法

     id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
        
     SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
     token.url = url;
     token.request = operation.request;
     token.downloadOperationCancelToken = downloadOperationCancelToken;
     token.downloader = self;
        
     return token;
    

    首先completedBlock是从callDownloadProcessForOperation里定义传到这里的下载成功后存储展示图片的block(progressBlock是从最外面传进来的,也就需要我们自己实现的。这里我们主要关注completedBlock),进入addHandlersForProgress方法

    - (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];
        SD_LOCK(self.callbacksLock);
        [self.callbackBlocks addObject:callbacks];
        SD_UNLOCK(self.callbacksLock);
        return callbacks;
    }
    

    反回了一个callbacks字典,两个元素对应的值分别是progressBlock和completedBlock, operation<SDWebImageDownloaderOperation>自己也通过callbackBlocks数组保存一下。

    然后创建了一个token,它的downloadOperationCancelToken就是callbacks,与operation和downloader建立关系,返回token ,赋值给上一层的operation.loaderOperation。

    到此这个downloadImageWithURL方法看完了,我们只看到在operation.completionBlock设置了operation从self.URLOperations移除。那么如果operation没有完成就被取消了呢?说明其他地方也有移除。

    我们全局搜一下URLOperations,发现在SDWebImageDownloader的cancel:方法里有移除

    - (void)cancel:(nullable SDWebImageDownloadToken *)token {
        NSURL *url = token.url;
        if (!url) {
            return;
        }
        SD_LOCK(self.operationsLock);
        NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
        if (operation) {
            BOOL canceled = [operation cancel:token.downloadOperationCancelToken];
            if (canceled) {
                [self.URLOperations removeObjectForKey:url];
            }
        }
        SD_UNLOCK(self.operationsLock);
    }
    

    cancel:方法被什么时候调用呢?继续搜cancel:在SDWebImageDownloadToken里我们发现调用了cancel:方法

    - (void)cancel {
        @synchronized (self) {
            if (self.isCancelled) {
                return;
            }
            self.cancelled = YES;
            if (self.downloader) {
                // Downloader is alive, cancel token
                [self.downloader cancel:self];
            } else {
                // Downloader is dealloced, only cancel download operation
                [self.downloadOperation cancel:self.downloadOperationCancelToken];
            }
            self.downloadOperationCancelToken = nil;
        }
    }
    

    那么SDWebImageDownloadToken的- (void)cancel方法什么时候被调用的呢?我们先看一下SDWebImageDownloadToken这个类,遵守了<SDWebImageOperation>协议,实现了cancel方法。loaderOperation是一个id<SDWebImageOperation>类型,token我们前面提到被返回赋值给了operation.loaderOperation。那么operation.loaderOperation调用cancel方法,也就是token调用了cancel方法。那么loaderOperation什么时候调用cancel方法呢?
    是不是它的拥有者operation取消的时候它也跟着取消呢?

    我们再全局搜一下loaderOperation什么时候调用了cancel方法

    @implementation SDWebImageCombinedOperation
    
    - (void)cancel {
        @synchronized(self) {
            if (self.isCancelled) {
                return;
            }
            self.cancelled = YES;
            if (self.cacheOperation) {
                [self.cacheOperation cancel];
                self.cacheOperation = nil;
            }
            if (self.loaderOperation) {
                [self.loaderOperation cancel];
                self.loaderOperation = nil;
            }
            [self.manager safelyRemoveOperationFromRunning:self];
        }
    }
    
    @end
    

    跟我们想的一样,在SDWebImageCombinedOperation(SDWebImageCombinedOperation也是遵守了<SDWebImageOperation>协议,实现了cancel方法)的cancel方法里有 [self.loaderOperation cancel],并且看到了safelyRemoveOperationFromRunning这句。所以我们又回到了前面看过opration< SDWebImageCombinedOperation >。

    所以我们再从新梳理一下整个过程:

    先回忆一下,manager是一个单例,整个过程manager.runningOperations只初始化了一次,manager.imageLoader只初始化过一次,manager.imageLoader.URLOperations也只初始化了一次,所以这几个是管理者,不管有多少个cell加载图片,这几个属性只有一个,也就是“管理者”。

    假设一种最麻烦的情况,一个cell正在加载一个图片,这个图片缓存里没有,需要下载图片,而这个图片正在下载时,cell被重用了

    1、首先调用sd_cancelImageLoadOperationWithKey方法,方法里获取到之前加载图片设置的operation(SDWebImageCombinedOperation),operation调用了自己的cancel方法,然后从sd_operationDictionary(runtime关联到imageView上的属性)移除。

    2、operation(SDWebImageCombinedOperation)的cancel方法里,自己的loaderOperation也cancel了,然后operation把自己从manager.runningOperations里移除。而loaderOperation就是token(SDWebImageDownloadToken),也就是token调用了自己的cancel方法。token自己的cancel方法里的downloader(就是manager.imageLoader下载器)调用了cancel:方法

    - (void)cancel:(nullable SDWebImageDownloadToken *)token {
        NSURL *url = token.url;
        if (!url) {
            return;
        }
        SD_LOCK(self.operationsLock);
        NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
        if (operation) {
            BOOL canceled = [operation cancel:token.downloadOperationCancelToken];
            if (canceled) {
                [self.URLOperations removeObjectForKey:url];
            }
        }
        SD_UNLOCK(self.operationsLock);
    }
    

    3、downloader的cancel:方法里,通过url从URLOperations取到上一个cell的下载图片的operation<SDWebImageDownloaderOperation>(这个是真正的多线程的operation,前面的都是抽象的)。operation调用了自己的cancel:方法,参数为downloadOperationCancelToken

    - (BOOL)cancel:(nullable id)token {
        BOOL shouldCancel = NO;
        SD_LOCK(self.callbacksLock);
        [self.callbackBlocks removeObjectIdenticalTo:token];
        if (self.callbackBlocks.count == 0) {
            shouldCancel = YES;
        }
        SD_UNLOCK(self.callbacksLock);
        if (shouldCancel) {
            [self cancel];
        }
        return shouldCancel;
    }
    

    传进来的downloadOperationCancelToken就是callbacks(两个元素对应的值分别是前一个cell的progressBlock和completedBlock,之前看过在哪保存的),而operation.callbackBlocks也保存了一份。然后通过对比地址移除,如果count不为0,说明其它的cell下载图片也用了这个operation,就是url相同的,所以是不应该cancel的。

    4、operation<SDWebImageDownloaderOperation>如果是应该取消的取消掉,manager.imageLoader.URLOperations移除operation。

    最后,既然下载图片是多线程的异步执行的,通过manager.imageLoader管理,我们看一下是怎么把图片正确的加载到对应的cell上的。每一个下载的operation<SDWebImageDownloaderOperation>,在调用自己的重写的start方时,把对应的下载task保存到了自己的dataTask属性。下载的downloadQueue和session属于manager.imageLoader的,我们看一下manager.imageLoader实现的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];
        }
    }
    

    然后进入operationWithTask方法

    - (NSOperation<SDWebImageDownloaderOperation> *)operationWithTask:(NSURLSessionTask *)task {
        NSOperation<SDWebImageDownloaderOperation> *returnOperation = nil;
        for (NSOperation<SDWebImageDownloaderOperation> *operation in self.downloadQueue.operations) {
            if ([operation respondsToSelector:@selector(dataTask)]) {
                if (operation.dataTask.taskIdentifier == task.taskIdentifier) {
                    returnOperation = operation;
                    break;
                }
            }
        }
        return returnOperation;
    }
    

    因为taskIdentifier是唯一的,并且在operation的start方法里对应的task都是新初始化的,所以不会出现重复的taskIdentifier。找到对应的operation,调用operation实现的- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error方法,最后成功下载到图片后进入下面方法

    - (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);
            }
        });
    }
    

    最后我们执行我们之前传进来的存储展示图片的completedBlock。那么这个cell被复用了呢?看一下前面的第三条就知道了,completedBlock被移除了就不会执行了。

    相关文章

      网友评论

        本文标题:SDWebImage源码解析(一)——如何处理cell重用

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