SDWebImage 加载图片流程


SDWebImage是老生常谈的三方,这篇博客算是一个笔记吧,记录下SDWebImage源码相关加载图片流程.

注1: 整体流程基于 SDWebImage 5.0.6 版本.

注2: 本文只对iOS执行流程进行分析.默认会去除 Mac开发的部分(带有 #if SD_UIKIT || SD_MAC).


SDWebImage 整体流程


我们通过官方的这张图可以看出整体流程,我们主要通过分类方法的形式直接接触SDWebImage的执行流程.

SDWebImage内部加载流程层级较多,所以我这里分为 对外流程SDWebImageManager 内部流程SDImageCache 内部流程SDWebImageDownloader 内部流程 四个模块进行执行代码流转说明.


对外流程


UIImageView + WebCache 或者 UIButton + WebCache

SDWebImage的三方入口通常是UIImageView + WebCache 或者 UIButton + WebCache 这种分类的 sd_setImageWithURL 方法.

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

不管是哪个分类的 sd_setImageWithURL 将统一进入 UIView + WebCache 的 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;


internalSetImageWithURL 内部流程
  • 判断当前视图上是否有正在进行的下载任务,如果有,那么就通过 sd_cancelImageLoadOperationWithKey 方法进行停止,对于其中的参数key,默认是使用类名.
    NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
    if (!validOperationKey) {
        validOperationKey = NSStringFromClass([self class]);
    }
    self.sd_latestOperationKey = validOperationKey;
    [self sd_cancelImageLoadOperationWithKey:validOperationKey]; // 停止先前的加载任务
    self.sd_imageURL = url;
  • 通过位运算判断传入参数 options 中是否包含 延时加载展位图(SDWebImageDelayPlaceholder), 如果没有,那么就返回主队列并且设置占位图.
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
        });
    }
  • 判断参数 url 是否为nil, 如果为真,那么就通过completedBlock直接返回错误信息.
        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);
            }
        });
  • 如果参数 url 不为nil,那么进入正常的加载流程.
    if (url) {
        ..... 正常流程
    } else {
        ..... 错误流程
    }
  • 首先对SDWebImageManager 进行懒加载.然后创建进度回调Block(combinedProgressBlock).
    SDWebImageManager *manager = context[SDWebImageContextCustomManager];
    if (!manager) {
        manager = [SDWebImageManager sharedManager];
    }   

    SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
        @strongify(self);
        NSProgress *imageProgress = self.sd_imageProgress;
        imageProgress.totalUnitCount = expectedSize;
        imageProgress.completedUnitCount = receivedSize;

        if (progressBlock) {
            progressBlock(receivedSize, expectedSize, targetURL);
        }
    };
  • 通过SDWebImageManager的 loadImageWithURL: 创建一个SDWebImageOperation,并且把它添加到当前视图任务HashMap中.这样如果后面会有新的任务,那么就可以通过key找到前次任务并且进行停止操作.
    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) {
    .... 完成回调
}];

    // 把图片加载Operation 加到 NSMapTable中.
    [self sd_setImageLoadOperation:operation forKey:validOperationKey];

  • 在SDWebImageOperation的completedBlock中主要来进行图片完成的设置.
    [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];


SDWebImageManager 内部流程


SDWebImageManager 中的 loadImageWithURL
  • 首先对 url 进行容错处理. 比如你的URL参数传入的是字符串类型,那么会内部转为NSURL类型. 再例如你传入其他类型就会置为nil.
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }
  • 创建一个 SDWebImageCombinedOperation 对象, 这个对象将会关联 Manager Cache 和 Download之间的关系.它有一个很重要的协议方法就是 cancel,用于停止加载图片的任务.
    SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    operation.manager = self;
  • 判断当前 URL 是否是一个失效的URL,例如先前下载过,但是任务失败会加入Manager单例中的集合属性 failedURLs 中. 所以我们判断 当前URL 是否在 failedURLs 这个集合中即可.
    BOOL isFailedUrl = NO;
    if (url) {
        SD_LOCK(self.failedURLsLock);
        isFailedUrl = [self.failedURLs containsObject:url];
        SD_UNLOCK(self.failedURLsLock);
    }
  • 根据上面的几种判断,例如URL错误,URL失效问题.如果成立,那么就在主队列中调用completedBlock 返回错误信息.
    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;
    }
  • 如果一切正常,那么就把当前 operation 添加到 runningOperations 这个集合属性中.
    SD_LOCK(self.runningOperationsLock);
    [self.runningOperations addObject:operation];
    SD_UNLOCK(self.runningOperationsLock);
  • 调用 callCacheProcessForOperation 方法,首先进行图片缓存查找.
    [self callCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];


SDWebImageManager 中的 callCacheProcessForOperation
  • 根据传入配置参数通过位运算判断 当前是否只允许下载模式.
    BOOL shouldQueryCache = (options & SDWebImageFromLoaderOnly) == 0;
  • 如果只是下载模式那么直接调用 callDownloadProcessForOperation 方法进入下载模式.
   [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
  • 如果是需要先查找缓存,那么根据 URL 通过MD5加密算法生成key.然后通过 SDImageCacheManager 单例通过 queryImageForKey 创建查询任务.并把任务Operation 返回赋值给 SDWebImageCombinedOperation 中的 cacheOperation 属性.
    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) {
        .... 查询完成回调
    }];
  • 在查询完成内部主要是判断是否已经查找到图片缓存,图片缓存的查找流程我们等一下再聊.如果图片缓存查找到就安全的移除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];


SDWebImageManager 中的 callDownloadProcessForOperation
  • 确定是否需要下载图片.依据有 配置选项中是否只有查找缓存的配置, 是否需要重新刷新图片缓存, 有没有实现图片下载的协议方法,url是否允许可以被下载 四个条件. 最终生成条件全部通过按位与的位运算生成的.
    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];
  • 首先我们先说一下,如果不走下载图片流程,那么程序就会判断参数中的 cacheImage 是否为 nil, 如果不为空,那么就会返回缓存图片,所以说 callCacheProcessForOperation 这个方法中不做完成返回的. 如果 cacheImage 是个nil,并且不允许下载代理,那么在 completedBlock 返回 nil.
    if (shouldDownload) {
        .... 正常下载流程
    }else if (cachedImage) {
        [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
        [self safelyRemoveOperationFromRunning:operation];
    } else {
        [self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
        [self safelyRemoveOperationFromRunning:operation];
    }
  • 正常的图片下载流程,首先判断参数 options 是否有图片缓存配置项(SDWebImageRefreshCached)和是否有缓存图片,如果有,那么就先把缓存图片传递回去.然后再进行图片下载工作.
    if (cachedImage && options & SDWebImageRefreshCached) {
        SDWebImageMutableContext *mutableContext;
        if (context) {
            mutableContext = [context mutableCopy];
        } else {
            mutableContext = [NSMutableDictionary dictionary];
        }
        mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage;
        context = [mutableContext copy];
    }
  • 通过SDWebImageDownLoader 单例进行通过 requestImageWithURL 图片网络请求.返回下载的operation,赋值给SDWebImageCombinedOperation对象中的 loaderOperation属性.
    @weakify(operation);
    operation.loaderOperation = [self.imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
        .... 完成回调      
    }];
  • 在requestImageWithURL完成回调中, 我们主要对返回结果进行处理,例如 ① 如果下载任务被取消,不进行任何操作; ② 如果是刷新缓存保存,也是不进行任何操作; ③ 如果是普通任务出错,那么就把url放入failedURLs中; ④ 如果完成图片下载任务, 根据配置options 判断是否从移除错误的URL.然后调用 callStoreCacheProcessForOperation 进入图片返回方法. 上述完成之后,安全移除operation.
    if (!operation || operation.isCancelled) {

    } else if (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) {

    } 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];
    }


SDWebImageManager 中的 callStoreCacheProccessForOperation

该方法是主要是进行缓存存储,缓存存储图片主要有两种形式,一种是通过Transform 处理过的图片,一种是普通的图片形式.

  • 首先进行transformer的对象取出以及生成 需要存入的key.
    SDImageCacheType storeCacheType = SDImageCacheTypeAll;
    if (context[SDWebImageContextStoreCacheType]) {
        storeCacheType = [context[SDWebImageContextStoreCacheType] integerValue];
    }
    id<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
    NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter];
    id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
    id<SDWebImageCacheSerializer> cacheSerializer = context[SDWebImageContextCacheSerializer];
  • 如果是transform转换的图片,那么会在全局异步并发队列中进行转换工作,然后通过 SDImageCacheManager 单例 storeImage 方法进行缓存存储.
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        @autoreleasepool {
            UIImage *transformedImage = [transformer transformedImageWithImage:downloadedImage forKey:key];
            if (transformedImage && finished) {
                NSString *transformerKey = [transformer transformerKey];
                NSString *cacheKey = SDTransformedKeyForKey(key, transformerKey);
                BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                NSData *cacheData;
                if (cacheSerializer && (storeCacheType == SDImageCacheTypeDisk || storeCacheType == SDImageCacheTypeAll)) {
                    cacheData = [cacheSerializer cacheDataWithImage:transformedImage  originalData:(imageWasTransformed ? nil : downloadedData) imageURL:url];
                } else {
                    cacheData = (imageWasTransformed ? nil : downloadedData);
                }
                [self.imageCache storeImage:transformedImage imageData:cacheData forKey:cacheKey cacheType:storeCacheType completion:nil];
            }
            [self callCompletionBlockForOperation:operation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
            }
        });
  • 如果不满足条件,那么就进行普通的存储.存储完成则通过callCompletedBlockWithOperation
    if (downloadedImage && finished) {
        if (cacheSerializer && (storeCacheType == SDImageCacheTypeDisk || storeCacheType == SDImageCacheTypeAll)) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                @autoreleasepool {
                    NSData *cacheData = [cacheSerializer cacheDataWithImage:downloadedImage originalData:downloadedData imageURL:url];
                    [self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key cacheType:storeCacheType completion:nil];
                }
            });
        } else {
            [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key cacheType:storeCacheType completion:nil];
        }
    }

    [self callCompletionBlockForOperation:operation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];


SDImageCache 内部流程


对于 SDImageCache 的流转入口主要是 queryImageForKey 获取图片 以及 storeImage 存储图片两个入口,我们一一来看.


SDImageCache 的 queryImageForKey
  • 方法首先对配置项进行操作转换 由 SDWebImageOptions 转为 SDImageCacheOptions.整体代码如下所示.
    SDImageCacheOptions cacheOptions = 0;
    if (options & SDWebImageQueryMemoryData) cacheOptions |= SDImageCacheQueryMemoryData;
    if (options & SDWebImageQueryMemoryDataSync) cacheOptions |= SDImageCacheQueryMemoryDataSync;
    if (options & SDWebImageQueryDiskDataSync) cacheOptions |= SDImageCacheQueryDiskDataSync;
    if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
    if (options & SDWebImageAvoidDecodeImage) cacheOptions |= SDImageCacheAvoidDecodeImage;
    if (options & SDWebImageDecodeFirstFrameOnly) cacheOptions |= SDImageCacheDecodeFirstFrameOnly;
    if (options & SDWebImagePreloadAllFrames) cacheOptions |= SDImageCachePreloadAllFrames;
  • 进入 queryCacheOperationForKey 方法.
    return [self queryCacheOperationForKey:key options:cacheOptions context:context done:completionBlock];


SDImageCache 的 queryImageOperationForKey
  • 首先先验证key,如果key为nil,直接block返回结果.
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }
  • 如果有transform操作,那么就对key进行处理操作.
    id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
    if (transformer) {
        NSString *transformerKey = [transformer transformerKey];
        key = SDTransformedKeyForKey(key, transformerKey);
    }
  • 从 SDMemoryCache 取出 图片缓存,对于 SDMemoryCache, 后面我们将一起看一下他的具体内部流程.
    UIImage *image = [self imageFromMemoryCacheForKey:key];
  • 判断当前图片是否是GIF动图,并且判断配置项中是否含有只对图片第一帧进行解码操作(SDImageCacheDecodeFirstFrameOnly).如果条件成立,就取出图片的第一帧并且返回.
    if ((options & SDImageCacheDecodeFirstFrameOnly) && image.sd_isAnimated) {
        image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
    }
  • 判断只查询缓存中的图片数据,如果是,那么就调用完成block 直接返回数据.
    BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryMemoryData));
    if (shouldQueryMemoryOnly) {
        if (doneBlock) {
            doneBlock(image, nil, SDImageCacheTypeMemory);
        }
        return nil;
    }
  • 如果上方没有返回,那么就查询沙盒图片数据,这里首先创建一个Operation对象,并且判断需要异步查询还是同步查询.
    NSOperation *operation = [NSOperation new];
    BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
                                (!image && options & SDImageCacheQueryDiskDataSync));
  • 创建查询沙盒数据Block.
    void(^queryDiskBlock)(void) =  ^{
    };
  • 查询沙盒数据Block主要是通过key来进行查询的.整个过程是在 @autoreleasepool 中进行的,所以能保证临时的数据对象及时释放.
    NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
  • 根据情况判断返回图片, ① 如果缓存中有图片数据,那么就直接启用缓存图片; ② 如果缓存中没有图片数据并且沙盒中有图片数据,那么首先先对图片进行解码操作.因为默认的图片结果过程只是在CPU提交之前才会进行解码,提前解码可以有效的减少卡顿. 然后再把图片存入缓存中.
    if (image) {
        diskImage = image;
        cacheType = SDImageCacheTypeMemory;
    } else if (diskData) {
        cacheType = SDImageCacheTypeDisk;
        diskImage = [self diskImageForKey:key data:diskData options:options context:context];
        if (diskImage && self.config.shouldCacheImagesInMemory) {
            NSUInteger cost = diskImage.sd_memoryCost;
            [self.memCache setObject:diskImage forKey:key cost:cost];
        }
    }
  • 判断是异步执行还是同步执行,如果是异步执行,通过GCD返回主线程然后返回数据.
    if (doneBlock) {
        if (shouldQueryDiskSync) {
            doneBlock(diskImage, diskData, cacheType);
        } else {
            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, diskData, cacheType);
            });
        }
    }
  • 查询沙盒的Block创建完成之后,根据配置项在串行队列中异步或者同步执行.
    if (shouldQueryDiskSync) {
        dispatch_sync(self.ioQueue, queryDiskBlock);
    } else {
        dispatch_async(self.ioQueue, queryDiskBlock);
    }


SDImageCache 的 storeImage

storeImage方法主要是用来存储图片缓存,根据参数进行缓存存储和沙盒存储.

  • 缓存存储数据比较简单,具体如下所示.
    if (toMemory && self.config.shouldCacheImagesInMemory) {
        NSUInteger cost = image.sd_memoryCost;
        [self.memCache setObject:image forKey:key cost:cost];
    }
  • 沙盒存储主要通过异步串行队列进行存储操作.
    dispatch_async(self.ioQueue, ^{
        @autoreleasepool {
            NSData *data = imageData;
            if (!data && image) {
                SDImageFormat format;
                if ([SDImageCoderHelper CGImageContainsAlpha:image.CGImage]) {
                    format = SDImageFormatPNG;
                } else {
                    format = SDImageFormatJPEG;
                }
                data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
            }
            [self _storeImageDataToDisk:data forKey:key];
        }
        
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });


SDWebImageDownLoader 内部流程


requestImageWithURL
  • 首先根据SDWebImageOptions 转换为 SDWebImageDownLoadOptions的配置.
    SDWebImageDownloaderOptions downloaderOptions = 0;
    if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
    if (options & SDWebImageProgressiveLoad) downloaderOptions |= SDWebImageDownloaderProgressiveLoad;
    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 (options & SDWebImageAvoidDecodeImage) downloaderOptions |= SDWebImageDownloaderAvoidDecodeImage;
    if (options & SDWebImageDecodeFirstFrameOnly) downloaderOptions |= SDWebImageDownloaderDecodeFirstFrameOnly;
    if (options & SDWebImagePreloadAllFrames) downloaderOptions |= SDWebImageDownloaderPreloadAllFrames;
    
    if (cachedImage && options & SDWebImageRefreshCached) {
        downloaderOptions &= ~SDWebImageDownloaderProgressiveLoad;
        downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
    }
  • 调用 downloadImageWithURL 进行图片下载.
    return [self downloadImageWithURL:url options:downloaderOptions context:context progress:progressBlock completed:completedBlock];


downloadImageWithURL
  • 首先仍然是判断 url 是否为nil, 如果为nil,那么就直接返回.
    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;
    }
  • 如果保证相同的url同时进行图片加载,只有一个下载任务呢?首先SDWebImageDownloader 中 有个字典URLOptions 用来存储当前所有的下载任务,key就是URL. 再就是每一次下载任务都是 在加锁条件下(信号量) 进行的,以前的两个操作就可以解决先前的问题.具体操作如下所示.
    SD_LOCK(self.operationsLock);
    NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];

    if (!operation || operation.isFinished || operation.isCancelled) {
            .... 相关操作
    }

    SD_UNLOCK(self.operationsLock);
  • 在创建的具体操作,主要是创建下载任务. 如果创建失败就会进行解锁通过completedBlock返回错误. 再就是会对任务完成的completedBlock进行赋值,Block里面主要是从URLOperations 中移除任务. 最后添加到 任务队列中 和 URLOperation 中.
    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;
    [self.downloadQueue addOperation:operation];
  • 上面说到一个问题是 同一时间 相同URL 只会创建一个Operation,那么回调completedBlock 和 进度progressBlock, SDWebImage又是如何管理的呢? 这个主要是存放在 SDWebImageDownloaderOperation 中的 数组属性 callBackBlocks, 数组中存放着字典 以 kProgressCallbackKey 和 kCompletedCallbackKey 为 键,存放着 完成 和 进度 Block. 外部入口调用如下所示.
    id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
  • 接着就是创建Token信息和返回Token.
    SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation];
    token.url = url;
    token.request = operation.request;
    token.downloadOperationCancelToken = downloadOperationCancelToken;
    token.downloader = self;
    return token;

到此,下载流程基本结束.


SDMemoryCache的相关


SDMemoryCache 是继承于 NSCache, NSCache具有线程安全,会根据内存警告自动做删减操作,不拷贝键等特点.

SDMemoryCache 含有一个NSMapTable类型的弱引用哈希表,这是为什么呢? 主要是因为 NSCache如果因为内存不足就会清理时,假设对象还没有被销毁,那么我们可以通过弱引用表找到该对象,增加了Cache的广度.


关于SDWebImage的提前对图片解码操作


在 SDImageCache 从沙盒读取出来的图片数据并不是直接存入缓存中,而是相对图片进行解码操作.这样可以减少后期CPU的计算工作.对提高性能有很大帮助.图片的解码工作主要是通过SDImageCoderHelper 类中的 decodedImageWithImagedecodedAndScaledDownImageWithImage 方法完成的.具体操作如下所示.

+ (UIImage *)decodedImageWithImage:(UIImage *)image {
    if (![self shouldDecodeImage:image]) {
        return image;
    }
    
    CGImageRef imageRef = [self CGImageCreateDecoded:image.CGImage];
    if (!imageRef) {
        return image;
    }
    UIImage *decodedImage = [[UIImage alloc] initWithCGImage:imageRef scale:image.scale orientation:image.imageOrientation];
    CGImageRelease(imageRef);
    decodedImage.sd_isDecoded = YES;
    decodedImage.sd_imageFormat = image.sd_imageFormat;
    return decodedImage;
}


关于SDWebImage的多线程问题


在 SDWebImage 的线程安全上基本上是用了信号量来实现线程安全.这个到处可见.如下所示.

    SD_LOCK(self.failedURLsLock);
    [self.failedURLs removeObject:url];
    SD_UNLOCK(self.failedURLsLock);

对于沙盒操作实际上是放在一个串行队列中.这样可以保证访问任务的顺序执行.对于异步还是同步主要是配置项的具体配置.

    _ioQueue = dispatch_queue_create("com.hackemist.SDImageCache", DISPATCH_QUEUE_SERIAL);


总结


SDWebImage 好几年前就翻了源码,但是只是简单的看了看,没有做记录,现在就接着面试的机会做个记录吧,如果有问题,欢迎指导~ 骚栋谢谢各位大佬了.


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342