美文网首页
移动端中图片的那些事(三)

移动端中图片的那些事(三)

作者: MichealXXX | 来源:发表于2020-01-06 10:37 被阅读0次

iOS开发历经了这么多年,说起关于图片的第三方库,相信大家首先想起的就是SDWebImage,可以说SDWebImage把图片加载优化到了方方面面,我们这一部分就来看看SDWebImage的源码,来看看它究竟做了些什么。

我们首先从最常使用的API开始,在我们加载网络图片的时候都会使用<SDWebImage/UIImageView+WebCache.h>中的这一个方法:

- (void)sd_setImageWithURL:(nullable NSURL *)url

我们再去<SDWebImage/UIImageView+WebCache.m>中去看一下它的源码

- (void)sd_setImageWithURL:(nullable NSURL *)url {
    [self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil];
}

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
}

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:nil];
}

- (void)sd_setImageWithURL:(nullable NSURL *)url completed:(nullable SDExternalCompletionBlock)completedBlock {
    [self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:completedBlock];
}

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:completedBlock];
}

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock {
    [self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:completedBlock];
}

这些方法都统一的调用了下面的方法:

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock {
    [self sd_internalSetImageWithURL:url
                    placeholderImage:placeholder
                             options:options
                        operationKey:nil
                       setImageBlock:nil
                            progress:progressBlock
                           completed:completedBlock];
}

而这个方法中调用了sd_internalSetImageWithURL:,这是核心的一句代码,之前的方法都是存在于UIImageView+WebCache文件中,而这一句核心代码的实现是在UIView+WebCache文件中,UIImageView+WebCache只是用于提供公共接口,而实现代码则是写在UIView+WebCache中,这么做的原因很简单,因为SDWebImage不仅仅可以为UIImageView加载图片,也可以为UIButton等视图控件加载图片,因此实现方法统一写在父类UIView中。

我们再来看看sd_internalSetImageWithURL:方法的内容:

- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                      operationKey:(nullable NSString *)operationKey
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                         completed:(nullable SDExternalCompletionBlock)completedBlock {
    return [self sd_internalSetImageWithURL:url placeholderImage:placeholder options:options operationKey:operationKey setImageBlock:setImageBlock progress:progressBlock completed:completedBlock context:nil];
}

其中又调用了本类的一个方法,也是最最核心的方法:

- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                      operationKey:(nullable NSString *)operationKey
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                         completed:(nullable SDExternalCompletionBlock)completedBlock
                           context:(nullable NSDictionary<NSString *, id> *)context {
    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }
    
    if (url) {
#if SD_UIKIT
        // check if activityView is enabled or not
        if ([self sd_showActivityIndicatorView]) {
            [self sd_addActivityIndicator];
        }
#endif
        
        // reset the progress
        self.sd_imageProgress.totalUnitCount = 0;
        self.sd_imageProgress.completedUnitCount = 0;
        
        SDWebImageManager *manager;
        if ([context valueForKey:SDWebImageExternalCustomManagerKey]) {
            manager = (SDWebImageManager *)[context valueForKey:SDWebImageExternalCustomManagerKey];
        } else {
            manager = [SDWebImageManager sharedManager];
        }
        
        __weak __typeof(self)wself = self;
        SDWebImageDownloaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
            wself.sd_imageProgress.totalUnitCount = expectedSize;
            wself.sd_imageProgress.completedUnitCount = receivedSize;
            if (progressBlock) {
                progressBlock(receivedSize, expectedSize, targetURL);
            }
        };
        id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            __strong __typeof (wself) sself = wself;
            if (!sself) { return; }
#if SD_UIKIT
            [sself sd_removeActivityIndicator];
#endif
            // if the progress not been updated, mark it to complete state
            if (finished && !error && sself.sd_imageProgress.totalUnitCount == 0 && sself.sd_imageProgress.completedUnitCount == 0) {
                sself.sd_imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
                sself.sd_imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
            }
            BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
            BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
                                      (!image && !(options & SDWebImageDelayPlaceholder)));
            SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
                if (!sself) { return; }
                if (!shouldNotSetImage) {
                    [sself sd_setNeedsLayout];
                }
                if (completedBlock && shouldCallCompletedBlock) {
                    completedBlock(image, error, cacheType, 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 = sself.sd_imageTransition;
            }
#endif
            dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
                [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
                [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
#endif
                callCompletedBlockClojure();
            });
        }];
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    } else {
        dispatch_main_async_safe(^{
#if SD_UIKIT
            [self sd_removeActivityIndicator];
#endif
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

看起来很长,很庞大的一个方法,但是不要被吓到,我们将它拆分开来一点一点的分析就能理解其中的奥秘。

先来看第一步:

//获取一个操作的key,如果没有获取到就将当前调用此方法的类名作为此次操作的key
 NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
//根据此次操作的key取消此前对应的下载任务
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
//使用关联对象为当前调用SDWebImage的类添加了一个属性,这个属性就是图片的地址
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

那么这个validOperationKey是什么呢?SDWebImage会将所有的操作记录在一个字典中,通过键值对来管理每一次操作,我们来看一下sd_cancelImageLoadOperationWithKey:这个方法,这是要根据key取消操作,因为如果调用方如果之前正在执行操作,避免一个调用方存在多个异步任务,因此要先cancel掉。这个方法存在于UIView+WebCacheOperation文件中,是专门用于管理操作的一个类,这个关联对象动态的为调用方增加了一个属性,用于记录图片的URL,以便之后获取。

- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    //如果存在key
    if (key) {
        // Cancel in progress downloader from queue
        //获取保存操作的字典
        SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
        id<SDWebImageOperation> operation;
        //对关键字典进行操作时上锁,方式其他线程同时修改,很多优秀的开源库对集合类型进行操作时都会进行加锁操作。
        @synchronized (self) {
            //根据key找到对应的操作
            operation = [operationDictionary objectForKey:key];
        }
        //如果找到操作  
        if (operation) {
            if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
                // 将当前操作取消
                [operation cancel];
            }
            @synchronized (self) {
                //在操作字典中删除对应的key
                [operationDictionary removeObjectForKey:key];
            }
        }
    }
}

我们继续看这个方法的源码第二步:

//如果没有设置默认图片延迟加载的选项,则在安全线程加载默认图片
if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }

第三步SDWebImageManager的初始化,SDWebImageManager和名字一样,它是SDWebImage的管理者,它有四个关键属性SDImageCache,SDWebImageDownloader,failedURLs,runningOperations分别管理图片的缓存下载失败的链接以及正在运行的操作

//如果有自定义的管理者就使用自定义管理者,否则使用单例进行初始化。
 SDWebImageManager *manager;
        if ([context valueForKey:SDWebImageExternalCustomManagerKey]) {
            manager = (SDWebImageManager *)[context valueForKey:SDWebImageExternalCustomManagerKey];
        } else {
            manager = [SDWebImageManager sharedManager];
        }

使用manager来进行下载操作,并用operation接收返回值,operation是遵守SDWebImageOperation协议的操作, 该协议只有一个取消方法.

//使用manager来进行下载操作
id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            __strong __typeof (wself) sself = wself;
            if (!sself) { return; }

            // 是否应该调用完成回调,下载完成或者设置了避免自动设置图片选项为YES
            BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
            // 是否应该不设置图片, 下载图片存在并且设置了避免自动设置图片选项 或者 下载图片不存在并且没有设置延迟加载占位图选项时 为YES;
            BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
                                      (!image && !(options & SDWebImageDelayPlaceholder)));
            SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
                if (!sself) { return; }
                if (!shouldNotSetImage) {
                    [sself sd_setNeedsLayout];
                }
                if (completedBlock && shouldCallCompletedBlock) {
                    completedBlock(image, error, cacheType, 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
                // 下载好了图片并且没有设置避免自动设置图片选项
                //将下载好的图片设置给目标view
                targetImage = image;
                targetData = data;
            } else if (options & SDWebImageDelayPlaceholder) {
                // case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
                //如果没下载成功并且设置延迟显示占位图则直接将占位图赋给目标view
                targetImage = placeholder;
                targetData = nil;
            }
            
            dispatch_main_async_safe(^{
                //调用设置图片方法,将图片赋值给对应的targetView
                [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];

                callCompletedBlockClojure();
            });
        }];
        //将操作与key绑定起来
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];

上面是简化后的具体方法,最主要的作用就是根据用户所选择的options来决定图片的显示方式,设置临时占位图,将操作以及其对应的key绑定放入操作字典中。

SDWebImageManager

现在我们就来看一下SDWebImageManager是如何进行图片的加载操作的,看一下它的核心方法loadImageWithURL

- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable 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) {
        LOCK(self.failedURLsLock);
        isFailedUrl = [self.failedURLs containsObject:url];
        UNLOCK(self.failedURLsLock);
    }

    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
        return operation;
    }

    LOCK(self.runningOperationsLock);
    [self.runningOperations addObject:operation];
    UNLOCK(self.runningOperationsLock);
    NSString *key = [self cacheKeyForURL:url];
    
    SDImageCacheOptions cacheOptions = 0;
    if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
    if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
    if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
    
    __weak SDWebImageCombinedOperation *weakOperation = operation;
    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
        __strong __typeof(weakOperation) strongOperation = weakOperation;
        if (!strongOperation || strongOperation.isCancelled) {
            [self safelyRemoveOperationFromRunning:strongOperation];
            return;
        }
        
        // Check whether we should download image from network
        BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly))
            && (!cachedImage || options & SDWebImageRefreshCached)
            && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL: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:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            }

            // download if no image or requested to refresh anyway, and download allowed by delegate
            SDWebImageDownloaderOptions downloaderOptions = 0;
            if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
            if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
            if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
            if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
            if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
            if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
            if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
            if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
            
            if (cachedImage && options & SDWebImageRefreshCached) {
                // force progressive off if image already cached but forced refreshing
                downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
                // ignore image read from NSURLCache if image if cached but force refreshing
                downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
            }
            
            // `SDWebImageCombinedOperation` -> `SDWebImageDownloadToken` -> `downloadOperationCancelToken`, which is a `SDCallbacksDictionary` and retain the completed block below, so we need weak-strong again to avoid retain cycle
            __weak typeof(strongOperation) weakSubOperation = strongOperation;
            strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                __strong typeof(weakSubOperation) strongSubOperation = weakSubOperation;
                if (!strongSubOperation || strongSubOperation.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 (error) {
                    [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock error:error url:url];
                    BOOL shouldBlockFailedURL;
                    // Check whether we should block failed url
                    if ([self.delegate respondsToSelector:@selector(imageManager:shouldBlockFailedURL:withError:)]) {
                        shouldBlockFailedURL = [self.delegate imageManager:self shouldBlockFailedURL:url withError:error];
                    } else {
                        shouldBlockFailedURL = (   error.code != NSURLErrorNotConnectedToInternet
                                                && error.code != NSURLErrorCancelled
                                                && error.code != NSURLErrorTimedOut
                                                && error.code != NSURLErrorInternationalRoamingOff
                                                && error.code != NSURLErrorDataNotAllowed
                                                && error.code != NSURLErrorCannotFindHost
                                                && error.code != NSURLErrorCannotConnectToHost
                                                && error.code != NSURLErrorNetworkConnectionLost);
                    }
                    
                    if (shouldBlockFailedURL) {
                        LOCK(self.failedURLsLock);
                        [self.failedURLs addObject:url];
                        UNLOCK(self.failedURLsLock);
                    }
                }
                else {
                    if ((options & SDWebImageRetryFailed)) {
                        LOCK(self.failedURLsLock);
                        [self.failedURLs removeObject:url];
                        UNLOCK(self.failedURLsLock);
                    }
                    
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
                    
                    // We've done the scale process in SDWebImageDownloader with the shared manager, this is used for custom manager and avoid extra scale.
                    if (self != [SDWebImageManager sharedManager] && self.cacheKeyFilter && downloadedImage) {
                        downloadedImage = [self scaledImageForKey:key image:downloadedImage];
                    }

                    if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
                        // Image refresh hit the NSURLCache cache, do not call the completion block
                    } else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
                        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                            UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

                            if (transformedImage && finished) {
                                BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                                NSData *cacheData;
                                // pass nil if the image was transformed, so we can recalculate the data from the image
                                if (self.cacheSerializer) {
                                    cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
                                } else {
                                    cacheData = (imageWasTransformed ? nil : downloadedData);
                                }
                                [self.imageCache storeImage:transformedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                            
                            [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                        });
                    } else {
                        if (downloadedImage && finished) {
                            if (self.cacheSerializer) {
                                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                                    NSData *cacheData = self.cacheSerializer(downloadedImage, downloadedData, url);
                                    [self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
                                });
                            } else {
                                [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                        }
                        [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                    }
                }

                if (finished) {
                    [self safelyRemoveOperationFromRunning:strongSubOperation];
                }
            }];
        } else if (cachedImage) {
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            [self safelyRemoveOperationFromRunning:strongOperation];
        } else {
            // Image not in cache and download disallowed by delegate
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
            [self safelyRemoveOperationFromRunning:strongOperation];
        }
    }];

    return operation;
}

依旧很长的一个方法,没关系,还是分解开来看。先看第一部分,这一部分是用于校验我们图片下载的URL是否有效。

// 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.
    //url是不是字符串类型,如果是字符串类型则强制转为URL类型。
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    // Prevents app crashing on argument type error like sending NSNull instead of NSURL
    //如果url不是NSURL类型,则将它赋值为nil,方式崩溃
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }
    //这里声明了一个叫做“组合操作”的operation,它的属性有我们的SDWebImageManager,还有一个cancel属性,一个存取缓存的线程属性,一个与下载有关的Token属性,后面会讲到,以及一个canel方法,用于终止操作。
    SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    operation.manager = self;

    //将是否是失败过的Url选项设置为NO
    BOOL isFailedUrl = NO;
    if (url) {
        //操作关键数组上锁
        LOCK(self.failedURLsLock);
        //如果这个url之前失败过则将选项设置为YES
        isFailedUrl = [self.failedURLs containsObject:url];
        UNLOCK(self.failedURLsLock);
    }

    //如果url的绝对字符串长度为0,并且没有设置失败重新下载,并且这个URL之前失败过则直接返回错位信息,下载失败。
    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
        return operation;
    }

URL经过一系列判断证明了自己是个可用的URL,接下来就开始进行了下载操作。

    //之前说过我们的manager有一个数组专门存储当前正在进行中的操作,url判断没问题,那么操作就要开始了,把当前的操作加入到这个数组中。
    LOCK(self.runningOperationsLock);
    [self.runningOperations addObject:operation];
    UNLOCK(self.runningOperationsLock);
    //先通过url当做key,去缓存字典中看看是不是之前有下载过并且留下了缓存。
    NSString *key = [self cacheKeyForURL:url];
    
    //根据缓存选项为cacheOptions赋值
    SDImageCacheOptions cacheOptions = 0;
    if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
    if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
    if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
    
    //防止循环引用使用weak修饰
    __weak SDWebImageCombinedOperation *weakOperation = operation;
    //这里大家注意,manager并不会直接去下载,而且会开启一个子线程,通过子线程去缓存中查找是否有图片的缓存。
    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
        __strong __typeof(weakOperation) strongOperation = weakOperation;
        //如果不存在操作或者操作被终止,将操作从正在进行的操作中移除
        if (!strongOperation || strongOperation.isCancelled) {
            [self safelyRemoveOperationFromRunning:strongOperation];
            return;
        }
        
        // Check whether we should download image from network
        //这里开始判断是不是要进行下载
        //通过是否有缓存,是否选择了刷新缓存等选项来判断是否要进行下载
        BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly))
            && (!cachedImage || options & SDWebImageRefreshCached)
            && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);

          // 如果在缓存中找到图片,但设置了SDWebImageRefreshCached选项,会通知缓存的图片,并尝试重新下载,以便让NSURLCache从服务器刷新它
          // 回调缓存图片然后重新下载
        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:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            }

            // download if no image or requested to refresh anyway, and download allowed by delegate
            // 如果没有图片或任何请求属性,并被代理允许下载,则去下载图片
            SDWebImageDownloaderOptions downloaderOptions = 0;
            if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
            if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
            if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
            if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
            if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
            if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
            if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
            if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
         

             //这里是设置下载选项使用的,如果存在缓存图片并且设置了刷新缓存选项
            if (cachedImage && options & SDWebImageRefreshCached) {
                // force progressive off if image already cached but forced refreshing
                // 如果缓存图片存在并强制刷新缓存,则强制关闭progressive
                downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
                // ignore image read from NSURLCache if image if cached but force refreshing
                // 忽略从NSURLCache读取的图像
                downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
            }
            
            // `SDWebImageCombinedOperation` -> `SDWebImageDownloadToken` -> `downloadOperationCancelToken`, which is a `SDCallbacksDictionary` and retain the completed block below, so we need weak-strong again to avoid retain cycle
  
            //此处避免循环引用设置weak修饰符
            __weak typeof(strongOperation) weakSubOperation = strongOperation;



             //*************从这里开始正式使用下载器开始下载
            //downloadToken用于标记下载任务便于管理,使用图片url以及下载选项进行图片的下载。
            strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                __strong typeof(weakSubOperation) strongSubOperation = weakSubOperation;
                //不存在操作或者操作被取消则不做任何操作
                if (!strongSubOperation || strongSubOperation.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 (error) {
                    //如果下载出现了错误,结束下载操作完成回调返回错误码
                    [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock error:error url:url];
                    //因为下载失败这里来判断是否要记录这个失败的URL
                    BOOL shouldBlockFailedURL;
                    // Check whether we should block failed url
                    if ([self.delegate respondsToSelector:@selector(imageManager:shouldBlockFailedURL:withError:)]) {
                        shouldBlockFailedURL = [self.delegate imageManager:self shouldBlockFailedURL:url withError:error];
                    } else {
                        shouldBlockFailedURL = (   error.code != NSURLErrorNotConnectedToInternet
                                                && error.code != NSURLErrorCancelled
                                                && error.code != NSURLErrorTimedOut
                                                && error.code != NSURLErrorInternationalRoamingOff
                                                && error.code != NSURLErrorDataNotAllowed
                                                && error.code != NSURLErrorCannotFindHost
                                                && error.code != NSURLErrorCannotConnectToHost
                                                && error.code != NSURLErrorNetworkConnectionLost);
                    }
                    
      
                    //如果判断要记录失败URL,将失败的URL加入失败数组中
                    if (shouldBlockFailedURL) {
                        LOCK(self.failedURLsLock);
                        [self.failedURLs addObject:url];
                        UNLOCK(self.failedURLsLock);
                    }
                }

                //**********这里开始是下载成功的操作
                else {
                    //如果之前下载失败过,但这次下载成功了,那么就把URL从失败数组中移除
                    if ((options & SDWebImageRetryFailed)) {
                        LOCK(self.failedURLsLock);
                        [self.failedURLs removeObject:url];
                        UNLOCK(self.failedURLsLock);
                    }
                    
                    //判断是不是要进行磁盘缓存
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
                    
                    // We've done the scale process in SDWebImageDownloader with the shared manager, this is used for custom manager and avoid extra scale.
                    //如果使用了自定义manager,则会进行图片的处理,因为默认的manager在下载器中会处理图片,避免再次进行图片处理
                    if (self != [SDWebImageManager sharedManager] && self.cacheKeyFilter && downloadedImage) {
                        downloadedImage = [self scaledImageForKey:key image:downloadedImage];
                    }

                    // 有缓存图片但没有下载的图片,并设置了刷新缓存,则不做处理
                    if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
                        // Image refresh hit the NSURLCache cache, do not call the completion block
                    } 

                    // 有下载的图片 && (没有动图 || 处理动图) &&  (下载之后,缓存之前处理图片)
                    else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {

//使用GCD来对图片进行处理
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                            //对图片进行处理
                            UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
                            //图片处理完毕
                            if (transformedImage && finished) {
                                BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                                NSData *cacheData;
                                // pass nil if the image was transformed, so we can recalculate the data from the image
                               // 缓存存储被处理的图片,如果已经处理了图片,则imageData设为nil,因此可以重新计算图片数据
                                if (self.cacheSerializer) {
                                    cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
                                } else {
                                    cacheData = (imageWasTransformed ? nil : downloadedData);
                                }

                                //缓存处理过的图片
                                [self.imageCache storeImage:transformedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                            
                            //完成回调,返回处理过的图片以及缓存
                            [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                        });
                    } else {
                        //完成下载,且无需处理图片
                        if (downloadedImage && finished) {
                            if (self.cacheSerializer) {
//使用GCD对图片进行缓存
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                                    NSData *cacheData = self.cacheSerializer(downloadedImage, downloadedData, url);
                                    [self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
                                });
                            } else {
                                [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                        }
                        [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                    }
                }
                //完成操作,将操作从操作数组中删除
                if (finished) {
                    [self safelyRemoveOperationFromRunning:strongSubOperation];
                }
            }];
        } else if (cachedImage) {
            //如果不需要下载且存在缓存则直接返回缓存图片
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            [self safelyRemoveOperationFromRunning:strongOperation];
        } else {
            // Image not in cache and download disallowed by delegate
            //缓存找不到图片并且下载不被允许则终结操作
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
            [self safelyRemoveOperationFromRunning:strongOperation];
        }
    }];

    return operation;

总结一下这一段的操作:

1.判断URL是不是可以使用,可以使用将operation加入正在运行的操作数组中。
2.operation开启子线程去查询缓存使用URL作为key查找缓存,判断是否查到缓存,有一些情况即使找到缓存也要继续下载,再判断是否要下载。
3.确定要下载,使用Downloader进行图片的下载,下载失败将URL加入到失败URL的数组中,下载成功进行下一步判断。
4.是否需要处理图片,如果需要处理图片,那么就在多线程GCD中处理图片处理完成缓存处理过的图片,如果不需要处理图片,直接缓存图片,完成操作将operation从操作数组中移除。
5.如果无需下载,那么直接从缓存中拿出图片缓存返回给UIImageView去使用。

使用到的技术细节:
1.对关键的集合类操作使用上锁解锁操作,防止其他线程侵入修改内容。
2.使用统一的操作数组管理正在进行的操作,以及使用failURLs数组记录曾经失败过的URL
3.耗时的缓存处理操作使用NSOperation技术在子线程中进行,处理图片的耗时操作在GCD中完成。
4.SDWebImage在每次图片请求时的判断都十分的严谨,是否要下载,还是直接取缓存图片,以及根据条件更新缓存。
5.在缓存时会判断是否要缓存在内存中,如果不是则缓存到磁盘中。

SDImageCache获取缓存

现在我们来看看SDWebImage的缓存操作,在加载图片的方法中,首先会使用queryCacheOperationForKey:去查询缓存,看一下这个方法的源码。

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    //没有缓存key,直接调用完成block,返回nil
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }
    
    // First check the in-memory cache...
    //首先在内存缓存中查找图片
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    //判断是否仅在内存缓存中查询图片,如果存在图片并且没有设置强制取磁盘图片,则判断仅仅在内存查找
    BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
    if (shouldQueryMemoryOnly) {
        if (doneBlock) {
            doneBlock(image, nil, SDImageCacheTypeMemory);
        }
        return nil;
    }
    
    //建立一个子线程
    NSOperation *operation = [NSOperation new];
    //在串行io队列中异步执行提取磁盘缓存操作
    void(^queryDiskBlock)(void) =  ^{
        if (operation.isCancelled) {
            // do not call the completion if cancelled
            return;
        }
        
        //在自动释放池中处理磁盘数据,防止临时对象太多造成内存溢出
        @autoreleasepool {
            //去磁盘中查找数据
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage;
            SDImageCacheType cacheType = SDImageCacheTypeDisk;
            //在内存中找到图片
            if (image) {
                // the image is from in-memory cache
                diskImage = image;
                cacheType = SDImageCacheTypeMemory;
            } else if (diskData) {
                // decode image data only if in-memory cache missed
                //在内存缓存中没找到图片,则去磁盘中读取数据编码图片
                diskImage = [self diskImageForKey:key data:diskData options:options];
                //如果找到了磁盘数据并且设置在内存缓存中进行保存
                if (diskImage && self.config.shouldCacheImagesInMemory) {
                    //将磁盘取出来的图片数据存储在内存缓存中。
                    NSUInteger cost = SDCacheCostForImage(diskImage);
                    [self.memCache setObject:diskImage forKey:key cost:cost];
                }
            }
            
            //如果存在完成block
            if (doneBlock) {
                //SDWebImage对内存缓存是同步的,磁盘缓存是异步的,如果强制磁盘同步存取则会同步存取
                if (options & SDImageCacheQueryDiskSync) {
                    doneBlock(diskImage, diskData, cacheType);
                } else {
                    //否则将异步获取磁盘缓存
                    dispatch_async(dispatch_get_main_queue(), ^{
                        doneBlock(diskImage, diskData, cacheType);
                    });
                }
            }
        }
    };
    
    //如果选择强制执行同步获取磁盘缓存则同步获取
    if (options & SDImageCacheQueryDiskSync) {
        queryDiskBlock();
    } else {
        //否则异步获取磁盘缓存
        dispatch_async(self.ioQueue, queryDiskBlock);
    }
    
    return operation;
}

我们来看看它获取缓存的步骤:

1.判断是否存在缓存key,没有直接执行完成block,返回nil。
2.存在key则先去内存缓存中查找图片,判断是不是仅仅在内存中查找就可以,如果在内存缓存中找到图片,执行完成block将内存缓存的图片返回。
3.如果内存缓存没有找到,或者用户设置同时要去磁盘中寻找,那么我们可以选择是异步去执行磁盘操作还是同步执行queryDiskBlock
4.如果在磁盘中找到了图片,则将磁盘中找到的数据提取出来存储在内存缓存中

再来看看这里面涉及到的一些知识点:

1.内存缓存的获取SDWebImage会默认同步进行,因为耗时短,但是磁盘的耗时操作就会选择使用GCD的异步执行。
2.将涉及大量编解码的操作放在自动释放池中执行,防止大量临时对象的产生造成内存溢出。
3.如果在磁盘中找到了缓存数据,也会将其取出存在内存缓存中方便提取操作。

SDImageCache进行缓存

再来看看图片下载完成的缓存操作:

- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock {
    //如果没有图片或者key,执行完成block
    if (!image || !key) {
        if (completionBlock) {
            completionBlock();
        }
        return;
    }
    // if memory cache is enabled
    //如果设置缓存在内存缓存,则将图片缓存在内存缓存中
    if (self.config.shouldCacheImagesInMemory) {
        NSUInteger cost = SDCacheCostForImage(image);
        [self.memCache setObject:image forKey:key cost:cost];
    }
    
    //如果设置图片要缓存在磁盘中
    if (toDisk) {
         //进行串行队列的异步操作
        dispatch_async(self.ioQueue, ^{
            @autoreleasepool {
                //获取图片数据
                NSData *data = imageData;
                //如果没有数据只有图片
                if (!data && image) {
                    // If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format

                    //如果我们没有任何数据检测图像格式,请检查它是否包含alpha通道以使用PNG或JPEG格式
                    SDImageFormat format;
                    if (SDCGImageRefContainsAlpha(image.CGImage)) {
                        format = SDImageFormatPNG;
                    } else {
                        format = SDImageFormatJPEG;
                    }
                    //将图片编码成相应的格式
                    data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:format];
                }
                //将图片存储到磁盘中
                [self _storeImageDataToDisk:data forKey:key];
            }
            
            //执行完成block
            if (completionBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock();
                });
            }
        });
    } else {
        if (completionBlock) {
            completionBlock();
        }
    }
}

这里要注意,将图片编码成相应的格式PNG或者是JPEG然后再进行磁盘数据的缓存。

SDWebImageDownloader

说完缓存,再看看如果没有缓存的情况下,下载操作是如何进行的

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    __weak SDWebImageDownloader *wself = self;

    return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
        __strong __typeof (wself) sself = wself;
        NSTimeInterval timeoutInterval = sself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

        // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
        //为了防止潜在的重复缓存(NSURLCache + SDImageCache),我们禁用图像请求的缓存
        NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;

        //创建下载请求
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
                                                                    cachePolicy:cachePolicy
                                                                timeoutInterval:timeoutInterval];
        
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        if (sself.headersFilter) {
            request.allHTTPHeaderFields = sself.headersFilter(url, [sself allHTTPHeaderFields]);
        }
        else {
            request.allHTTPHeaderFields = [sself allHTTPHeaderFields];
        }

        //创建下载操作
        SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
        operation.shouldDecompressImages = sself.shouldDecompressImages;
        
        if (sself.urlCredential) {
            operation.credential = sself.urlCredential;
        } else if (sself.username && sself.password) {
            operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
        }
        //设置下载优先级
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }
        
        //模拟后进先出
        if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
            [sself.lastAddedOperation addDependency:operation];
            sself.lastAddedOperation = operation;
        }

        return operation;
    }];
}

注意这里为什么要后进先出,假如我们是一个滑动界面,当我们在第一页还未下载成功时迅速滑动到下一页,如果是常规的FIFO那么就会在我们观看第二页时依旧在执行上一页的图片下载操作。

下载操作执行addProgressCallback:的实现代码:

- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                                                   forURL:(nullable NSURL *)url
                                           createCallback:(SDWebImageDownloaderOperation *(^)(void))createCallback {
    // 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 != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return nil;
    }
    
    LOCK(self.operationsLock);

    //根据URL获取一个下载操作
    SDWebImageDownloaderOperation *operation = [self.URLOperations objectForKey:url];
    // There is a case that the operation may be marked as finished, but not been removed from `self.URLOperations`.
    if (!operation || operation.isFinished) {
        //操作若不存在则创建一个下载操作
        operation = createCallback();
        __weak typeof(self) wself = self;
        operation.completionBlock = ^{
            __strong typeof(wself) sself = wself;
            if (!sself) {
                return;
            }
            LOCK(sself.operationsLock);
            //操作完成则从操作字典中移除本次操作
            [sself.URLOperations removeObjectForKey:url];
            UNLOCK(sself.operationsLock);
        };
        //将本次下载操作加入记录操作的字典中。
        [self.URLOperations setObject:operation forKey:url];
        // 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];
    }
    UNLOCK(self.operationsLock);

    id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
    
    // 保存progressBlock和completedBlock
    SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
    token.downloadOperation = operation;
    token.url = url;
    token.downloadOperationCancelToken = downloadOperationCancelToken;

    return token;
}

SDWebImageDownloader负责对下载操作进行设置,而真正的下载操作则是由SDWebImageDownloaderOperation来执行,整个的下载流程都是使用NSOperation来进行异步下载,下载完成后图片会根据用户的选项存入内存缓存亦或是磁盘缓存。

SDWebImageCodersManager

我们为UIImageView设置图片时,无论SDWebImage是从磁盘获取缓存,还是从网络下载图片都会调用SDWebImageCodersManager中的一个方法decompressedImageWithImage:这个方法就是我上一篇笔记中提到的,异步将图片进行解压缩然后进行渲染。

- (nullable UIImage *)sd_decompressedImageWithImage:(nullable UIImage *)image {
    if (![[self class] shouldDecodeImage:image]) {
        return image;
    }
    
    // autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
    // on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
    //自动释放掉位图掉上下文以及所有的实例帮助系统释放内存控件
    @autoreleasepool{
        
        CGImageRef imageRef = image.CGImage;
        // device color space
        //设置颜色空间为RGB
        CGColorSpaceRef colorspaceRef = SDCGColorSpaceGetDeviceRGB();
        //判断图片是否支持alpha通道
        BOOL hasAlpha = SDCGImageRefContainsAlpha(imageRef);
        // iOS display alpha info (BRGA8888/BGRX8888)
        //设置字节顺序为32位
        CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
        //当图片不包含 alpha的时候使用kCGImageAlphaNoneSkipFirst ,否则使用 kCGImageAlphaPremultipliedFirst
        bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
        //位图的宽度和高度,分别赋值为图片的像素宽度和像素高度
        size_t width = CGImageGetWidth(imageRef);
        size_t height = CGImageGetHeight(imageRef);
        
        // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
        // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
        // to create bitmap graphics contexts without alpha info.
        //创建位图上下文
        //第一个参数data, 设置NULL ,那么系统就会为我们自动分配和释放所需的内存
        //后两个填位图的位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可
        //kBitsPerComponent为8,在像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8 
        //位图的每一行使用的字节数,大小至少为 width * bytes per pixel 字节。有意思的是,当我们指定 0 时,系统不仅会为我们自动计算,而且还会进行 cache line alignment 的优化
        //设置颜色空间为RGB
        //设置位图布局信息。
        CGContextRef context = CGBitmapContextCreate(NULL,
                                                     width,
                                                     height,
                                                     kBitsPerComponent,
                                                     0,
                                                     colorspaceRef,
                                                     bitmapInfo);
        if (context == NULL) {
            return image;
        }
        
        // Draw the image into the context and retrieve the new bitmap image without alpha
        //根据我们的参数设置绘制图片,将其解压,达到子线程强制解压缩,避免主线程解压造成性能问题。
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
        CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
        UIImage *imageWithoutAlpha = [[UIImage alloc] initWithCGImage:imageRefWithoutAlpha scale:image.scale orientation:image.imageOrientation];
        //记得释放CG类型数据,CG类型数据的内存管理依旧需要我们手动管理
        CGContextRelease(context);
        CGImageRelease(imageRefWithoutAlpha);
        
        return imageWithoutAlpha;
    }
}
总结

仅仅分析了部分功能,SDWebImage就使用到了相当多的iOS知识点:

1.对集合类的使用非常多,失败URL的数组,记录操作的字典,正在运行线程的数组等等,而且对集合类的管理及其细致,无论加入还是移除的判断都考虑的面面俱到。
2.对于的使用,由于使用了很多的多线程技术,因此在操作集合类的时候都会选择上锁,防止其他线程同时进行修改,使用到了@synchronized以及GCDdispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER)来对集合类进行锁的操作。
3.多线程的使用,无论是下载还是缓存操作都使用到多线程,下载使用了NSOperation而缓存使用的则是GCD的dispatch_async
4.内存管理的应用,在ARC时代很少会需要手动键入内存管理语句,而SDWebImage则在可能产生大量临时变量的时候使用自动释放池,并且手动管理CG类型
5.优化图片渲染的流程,异步强行解压缩位图,避免主线程解压影响性能。
6.运行时的应用,使用关联对象为类增加属性。
7.运用了多种设计模式。

相关文章

  • 移动端中图片的那些事(三)

    iOS开发历经了这么多年,说起关于图片的第三方库,相信大家首先想起的就是SDWebImage,可以说SDWebIm...

  • 移动端中图片的那些事(一)

    在我们的app中图片的加载是影响性能的一大原因,尤其是在复杂的列表,瀑布流的界面中加载图片,可以说如果不做一些优化...

  • 移动端中图片的那些事(二)

    在我们的APP运行中,图片资源一直都是最消耗性能的因素之一,一张图片从磁盘加载到UIImageView上面要经历以...

  • 移动端的那些事(一)

    移动端纯纯纯纯小白一个,单纯做下记录。 常用meta标签 input,button 在ios上会有圆角 a,inp...

  • Unity 图片资源格式的设置

    前言 为什么移动端需要压缩图片 移动处理器带宽非常低. 因此移动处理中的图片压缩就非常重要. 特别是对于低端手机,...

  • iOS移动端架构的那些事

    引言:一个app的初始阶段,必然是先满足各种业务需求。然后,经过多次版本迭代之后,先前的由于急于满足需求而导致的杂...

  • 按比例缩放的布局

    需求: 移动端图片宽高1:1显示.并且图片宽度要充满手机屏幕的宽度. 移动端banner 按比例显示 不固定高度 ...

  • 移动端图片格式调研

    移动端图片格式调研 图片通常是移动端流量耗费最多的部分,并且占据着重要的视觉空间。合理的图片格式选用和优化可以为你...

  • Glide动态获取宽高,自动适配瀑布流布局,解决滑动跳动

    所遇到的问题: 问题:服务端返回的图片没有返回尺寸,移动端无法确定图片比例,导致RecyclerView的图片无法...

  • website-bug-video

    Bug:网页PC端和移动端背景不同; PC端背景为视频,移动端背景为图片切换动画。从手机上某个app上点击网址打开...

网友评论

      本文标题:移动端中图片的那些事(三)

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