读源码-SDWebImage

SDWebImage在github上有19k+星星。这个库提供了一个支持缓存的异步图像下载器。提供UIImageView和UIButton分类,开发者只需要简单的调用公共接口,就可以实现强大的图片异步下载及缓存的功能。

以下内容,从UIImageView+WebCache分类公共接口开始,慢慢延伸到方法的具体实现,从中学习了作者的图片下载缓存策略。


UIImageView+WebCache公共接口
- (void)sd_setImageWithURL:(nullable NSURL *)url
- (void)sd_setImageWithURL:(nullable NSURL *)url
        placeholderImage:(nullable UIImage *)placeholder
- (void)sd_setImageWithURL:(nullable NSURL *)url
        placeholderImage:(nullable UIImage *)placeholder
        options:(SDWebImageOptions)options
- (void)sd_setImageWithURL:(nullable NSURL *)url
        completed:(nullable SDExternalCompletionBlock)completedBlock
- (void)sd_setImageWithURL:(nullable NSURL *)url
        placeholderImage:(nullable UIImage *)placeholder
        completed:(nullable SDExternalCompletionBlock)completedBlock
- (void)sd_setImageWithURL:(nullable NSURL *)url
        placeholderImage:(nullable UIImage *)placeholder
        options:(SDWebImageOptions)options
        completed:(nullable SDExternalCompletionBlock)completedBlock
- (void)sd_setImageWithURL:(nullable NSURL *)url
        placeholderImage:(nullable UIImage *)placeholder
        options:(SDWebImageOptions)options
        progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
        completed:(nullable SDExternalCompletionBlock)completedBlock

上面的接口方法内部都是调用的下面这个方法,该方法是在UIView+WebCache分类方法里,方便UIButton和UIImageView都能调用

- (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

上面方法内部又调用了下面的方法, 此方法真正具体实现处理下载图片显示逻辑

- (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 *)context

接下来看一下方法内部的具体实现
- (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 *)context {

    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);

    [self sd_cancelImageLoadOperationWithKey:validOperationKey]; // 从operationDictionary中找到validOperationKey对应的操作,从队列中取消该操作并移除。保证当前没有正在进行的异步下载操作。

    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // 设置关联对象url

    if (!(options & SDWebImageDelayPlaceholder)) { // 如果 没有 设置延迟添加占位图片选项
        if ([context valueForKey:SDWebImageInternalSetImageGroupKey]) {
            dispatch_group_t group = [context valueForKey:SDWebImageInternalSetImageGroupKey]; // 获取调度组
            dispatch_group_enter(group); // 进入
        }
        dispatch_main_async_safe(^{ // 保证在主线程中执行代码
            // 添加临时的占位图
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }

    if (url) { // url存在
        // check if activityView is enabled or not
        if ([self sd_showActivityIndicatorView]) { // 检查活动指示器视图是否启用
            [self sd_addActivityIndicator]; // 添加活动指示器视图,并让活动指示器动画
        }

        SDWebImageManager *manager;// 如果有自定义的管理者,则用自定义的管理者,否则,用单例管理对象
        if ([context valueForKey:SDWebImageExternalCustomManagerKey]) {
            manager = (SDWebImageManager *)[context valueForKey:SDWebImageExternalCustomManagerKey];
        } else {
            manager = [SDWebImageManager sharedManager];
        }


        __weak __typeof(self)wself = self; // 用__weak修饰,防止循环引用
        // 用SDWebImageManager下载图片,并用operation接收返回值,operation是遵守SDWebImageOperation协议的操作, 该协议只有一个取消方法.
        id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { // 下载完成回调

            __strong __typeof (wself) sself = wself; // 用__strong修饰,防止self被释放

            [sself sd_removeActivityIndicator]; // 移除活动指示器

            if (!sself) { return; } // 保证下面的代码安全

            BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage); // 是否应该调用完成回调,下载完成或者设置了避免自动设置图片选项是为YES

            BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
        (!image && !(options & SDWebImageDelayPlaceholder)));
            // 是否应该不设置图片, 下载图片存在并且设置了避免自动设置图片选项 或者 下载图片不存在并且没有设置延迟加载占位图选项时 为YES;

            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; // 目标image
            NSData *targetData = nil;
            if (image) {
                // case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
                // 下载好了图片并且没有设置避免自动设置图片选项
                targetImage = image; // 将下载好的图片设置为目标image
                targetData = data;
            } else if (options & SDWebImageDelayPlaceholder) {
                // case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
                // 图片不存在并且设置了延迟加载占位图选项
                targetImage = placeholder; // 将占位图设置为目标image
                targetData = nil;
            }

            if ([context valueForKey:SDWebImageInternalSetImageGroupKey]) { //如果 没有 设置延迟加载占位图片选项 才会有调度组
                dispatch_group_t group = [context valueForKey:SDWebImageInternalSetImageGroupKey]; // 获取调度组
                dispatch_group_enter(group); // 进入组
                dispatch_main_async_safe(^{ // 安全回到主线程异步处理
                    // 设置目标图片
                    // 如果有setImageBlock,则不会设置目标图片,而是用setImageBlock回调给开发者自己去设置
                    [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                });
                // ensure completion block is called after custom setImage process finish
                // 确保完成的block调用是在自定义设置图片过程完成之后
                dispatch_group_notify(group, dispatch_get_main_queue(), ^{
                    callCompletedBlockClojure();
                });
            } else { // 没有调度组,即设置了延迟加载占位图
                dispatch_main_async_safe(^{
                    // 设置目标图片
                    // 如果有setImageBlock,则不会设置目标图片,而是用setImageBlock回调给开发者自己去设置
                    [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                    // 完成回调
                    callCompletedBlockClojure();
                });
            }
        }];
        // 将operation操作与validOperationKey关联起来,作为键值对保存在operationDictionary字典中,表示当前的操作正在进行
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    } else { // url == nil
        dispatch_main_async_safe(^{
            [self sd_removeActivityIndicator]; // 移除活动指示器
            if (completedBlock) { // 回调错误信息
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

该方法的作用:

  1. 避免多个异步加载任务同时存在
  2. 根据需要设置临时占位图
  3. 活动指示器在合适的时机显示与隐藏
  4. 调用SDWebImageManager来加载图片
  5. 将下载操作存放到正在进行的操作字典中

其中从队列中取消正在下载的操作内部实现如下:
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    // Cancel in progress downloader from queue

    SDOperationsDictionary *operationDictionary = [self sd_operationDictionary]; // 获取操作字典
    id<SDWebImageOperation> operation; // 操作
    @synchronized (self) { // 加线程锁 保证线程安全
        operation = [operationDictionary objectForKey:key]; // 根据key 取出操作
    }
    if (operation) {
        if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]){
            [operation cancel]; // 取消此操作
        }
        @synchronized (self) {
            [operationDictionary removeObjectForKey:key]; // 移除key
        }
    }
}
SDWebImageManager加载图片的内部实现如下:
// 加载图片
- (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.
    // 如果传入的url是NSString类型,就转化为NSURL类型。
    // 很常见的错误是,开发者将NSString对象代替NSURL对象作为URL传递,出于一些奇怪的原因,Xcode不会为这种类型的不匹配而抛出警告。以下方法允许url作为NSString类型来传递
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    // Prevents app crashing on argument type error like sending NSNull instead of NSURL
    // 确保url正确,防止app在参数类型错误时崩溃,例如传递了一个NSNull代替NSURL
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }

    __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; // 组合操作
    __weak SDWebImageCombinedOperation *weakOperation = operation;

    BOOL isFailedUrl = NO; // url是否失效的标记
    if (url) { // 如果url存在
        @synchronized (self.failedURLs) {
            isFailedUrl = [self.failedURLs containsObject:url]; // 失效url名单中若包含此url 则标记为YES
        }
    }

    // url 的绝对字符串为0 或者 (没有设置重置失败选项 并且 isFailedUrl为YES) 则直接调用完成的操作并返回错误信息
    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有效
    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation]; // 将操作添加到正在进行的操作数组中
    }

    // 根据url返回对应的缓存key
    NSString *key = [self cacheKeyForURL:url];

    // 组合操作的缓存操作, 根据缓存key获取(使用self.imageCache查询是否存在缓存图片)
    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
        if (operation.isCancelled) {  // 如果操作被取消
            // 安全从正在进行下载操作数组中移除此操作
            [self safelyRemoveOperationFromRunning:operation];
            return;
        }

        // 如果(没有缓存图片或者设置了重置缓存选项) 并且 (代理对象没有响应mageManager:shouldDownloadImageForURL:方法默认为YES或者代理对象响应该方法时返回YES --》 需要下载图片
        if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {

            // 存在缓存图片 并且需要重置缓存选项(有缓存图片也要下载更新图片)
            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.
                // 如果在缓存中找到图片,但设置了SDWebImageRefreshCached选项,会通知缓存的图片,并尝试重新下载,以便让NSURLCache从服务器刷新它
                // 回调缓存图片然后重新下载
                [self callCompletionBlockForOperation:weakOperation 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;
            }

            // 使用图片下载器下载
            //subOperationToken用来标记当前的下载任务,便于被取消
            SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                if (!strongOperation || strongOperation.isCancelled) {
                    // 操作被取消则不做任何处理, 避免和其他的completedBlock重复
                    // 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:strongOperation completion:completedBlock error:error url:url];

                    if (   error.code != NSURLErrorNotConnectedToInternet
                        && error.code != NSURLErrorCancelled
                        && error.code != NSURLErrorTimedOut
                        && error.code != NSURLErrorInternationalRoamingOff
                        && error.code != NSURLErrorDataNotAllowed
                        && error.code != NSURLErrorCannotFindHost
                        && error.code != NSURLErrorCannotConnectToHost
                        && error.code != NSURLErrorNetworkConnectionLost) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs addObject:url]; // 将url添加到记录失效url的名单中
                        }
                    }
                }
                else {  // 下载成功
                    if ((options & SDWebImageRetryFailed)) { // 设置了 下载失败后尝试重新下载选项,
                        @synchronized (self.failedURLs) {
                            [self.failedURLs removeObject:url]; // 从失效url名单中移除url
                        }
                    }

                    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.
                    // 如果不是共享管理器并有缓存key过滤和下载的图片,缩放图片
                    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];
                                // pass nil if the image was transformed, so we can recalculate the data from the image
                                // 缓存存储被处理的图片,如果已经处理了图片,则imageData设为nil,因此可以重新计算图片数据
                                [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
                            }

                            // 将图片传入完成回调
                            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                        });
                    } else {
                        // 不需要处理图片
                        if (downloadedImage && finished) {
                            // 将下载好的图片保存到缓存中
                            [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                        }
                        // 再执行完成回调
                        [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                    }
                }

                if (finished) {
                    // 下载完成 从正在下载的操作数组中移除本次下载操作
                    [self safelyRemoveOperationFromRunning:strongOperation];
                }
            }];
            @synchronized(operation) {
                // Need same lock to ensure cancelBlock called because cancel method can be called in different queue
                // 需要线程锁才能确保cancelBlock被调用,因为可以在不同的队列中调用cancel方法
                operation.cancelBlock = ^{
                    // 取消下载任务
                    [self.imageDownloader cancel:subOperationToken];
                    __strong __typeof(weakOperation) strongOperation = weakOperation;
                    // 从正在下载的操作数组中移除本次下载操作
                    [self safelyRemoveOperationFromRunning:strongOperation];
                };
            }
        } else if (cachedImage) { // 有缓存图片
            __strong __typeof(weakOperation) strongOperation = weakOperation;
            // 使用缓存图片回调
            [self callCompletionBlockForOperation:strongOperation 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
            // 回调图片为nil
            __strong __typeof(weakOperation) strongOperation = weakOperation;
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
            // 从正在下载的操作数组中移除此操作
            [self safelyRemoveOperationFromRunning:operation];
        }
    }];

    return operation;
}

SDWebImageManager加载图片的过程:

  1. 确保url传递正确
  2. 对失效url的处理
  3. 根据key从缓存中查找对应的缓存图片
    3.1 如果有缓存图片则使用缓存图片
    3.2 如果没有缓存图片并且没有下载到图片则图片为nil
    3.3 没有缓存图片但下载成功的处理
    3.3.1 如果只做内存缓存,则把下载好的图片保存到内存中
    3.3.2 否则,将下载好的图片先保存到内存中,再将图片数据保存到磁盘中。
    3.3.3 回调使用下载图片

查询缓存图片方法内部实现如下:

// 根据缓存key 现在内存中查询图片,如果存在,则用内存中的图片执行回调,否则,去磁盘缓存中查找图片,如果找到了,并且需要设置为内存缓存,则先保存到内存缓存中,然后用磁盘缓存图片执行回调
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {

    // 如果key不存在,有回调则直接回调 没有图片缓存
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }

    // First check the in-memory cache...
    // 首先根据key在内存缓存中查询图片
    UIImage *image = [self imageFromMemoryCacheForKey:key];

    // 如果图片存在
    if (image) {
        NSData *diskData = nil;
        // 如果是动画图片,则根据key获取磁盘数据,后面传到完成的回调中,如果不是就传nil
        if (image.images) {
            diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        }
        if (doneBlock) { // 有完成的回调调用回调 内存图片,磁盘数据,缓存类型:内存缓存
            doneBlock(image, diskData, SDImageCacheTypeMemory);
        }
        return nil;
    }

    // 内存缓存中没有找到图片
    NSOperation *operation = [NSOperation new]; // 创建操作
    dispatch_async(self.ioQueue, ^{ // 在串行ioQueue队列中异步执行block操作
        if (operation.isCancelled) {
            // do not call the completion if cancelled
            // 如果操作被取消,则不调用完成的回调
            return;
        }

        @autoreleasepool {
            // 根据key获取磁盘数据
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            // 根据key在磁盘中查找图片
            UIImage *diskImage = [self diskImageForKey:key];
            // 如果图片存在 并且 配置了应该在内存中缓存图片
            if (diskImage && self.config.shouldCacheImagesInMemory) {
                // 获取图片的内存成本
                NSUInteger cost = SDCacheCostForImage(diskImage);
                // 在内存中缓存图片
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }
            // 有完成回调
            if (doneBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    // 主线程中异步执行 执行回调 磁盘图片,磁盘内存,缓存类型为磁盘缓存
                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                });
            }
        }
    });

    return operation;
}

查找缓存图片逻辑:
先从内存缓存中查找缓存图片,如果有就使用内存缓存图片,否则去磁盘缓存中查找缓存图片,如果找到了,并配置了应该在内存中缓存图片选项,就先保存到内存缓存中,然后再使用缓存图片。


将下载好的图片保存到缓存的方法实现如下:
// 缓存下载图片
- (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)) { // 包含alpha通道的为PNG格式否则为JPEG格式
                        format = SDImageFormatPNG;
                    } else {
                        format = SDImageFormatJPEG;
                    }
                    // 将图片按照对应格式编码为数据类型
                    data = [[SDWebImageCodersManager sharedInstance] encodedDataWithImage:image format:format];
                }
                // 将图片数据保存到磁盘中
                [self storeImageDataToDisk:data forKey:key];
            }
                // 执行保存完成回调
            if (completionBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock();
                });
            }
        });
    } else {
        if (completionBlock) {
            completionBlock();
        }
    }
}

存储下载图片到缓存的逻辑: 如果配置了启用内存缓存,则先保存到内存中,如果没有标记只做内存缓存,则需要保存到磁盘中,那么就将图片按照对应格式转换为NSData类型存储到磁盘文件里。


使用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; // 默认下载超时时间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.HTTPHeaders copy]);
        }
        else {
            request.allHTTPHeaderFields = sself.HTTPHeaders;
        }
        // 创建下载操作
        SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
        operation.shouldDecompressImages = sself.shouldDecompressImages;

        if (sself.urlCredential) {
            operation.credential = sself.urlCredential; // 设置url凭证
        } 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;
        }

        // 将操作添加到下载队列中
        [sself.downloadQueue addOperation:operation];

        //如果是LIFO - 后进先出
        if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
            // 通过系统地添加新操作作为最后一个操作的依赖关系来模拟LIFO执行顺序
            [sself.lastAddedOperation addDependency:operation];
            sself.lastAddedOperation = operation;
        }

        return operation;
    }];
}

保存progressBlock方法内部实现:
// 保存progressBlock
- (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.
    // 如果url不存在,立即执行完成的block
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return nil;
    }

    __block SDWebImageDownloadToken *token = nil;

    dispatch_barrier_sync(self.barrierQueue, ^{
        SDWebImageDownloaderOperation *operation = self.URLOperations[url]; // 获取下载操作
        if (!operation) { // 操作若不存在则创建一个下载操作
            operation = createCallback();
            self.URLOperations[url] = operation;

            __weak SDWebImageDownloaderOperation *woperation = operation;
            operation.completionBlock = ^{
                dispatch_barrier_sync(self.barrierQueue, ^{
                    SDWebImageDownloaderOperation *soperation = woperation;
                    if (!soperation) return;
                    if (self.URLOperations[url] == soperation) {
                        // 下载操作完成,从操作字典中移除此操作
                        [self.URLOperations removeObjectForKey:url];
                    };
                });
            };
        }
        // 保存progressBlock和completedBlock
        id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];

        token = [SDWebImageDownloadToken new];
        token.url = url;
        token.downloadOperationCancelToken = downloadOperationCancelToken;
    });

    return token;
}

总结:

SDWebImageManager同时管理SDImageCacheSDWebImageDownloader,负责协调加载图片时缓存图片和下载图片的任务。
加载图片大体工作流程是: 首先查询是否存在缓存,如果有缓存,则使用缓存的图片;否则,使用SDWebImageDownloader下载器来下载图片,下载成功后,先保存到缓存里,再显示图片。
SDImageCache管理缓存的查询和存储。查询时: 先看内存中有没有缓存图片,如果有,就用内存缓存图片,若内存中没有,就去磁盘中查找,如果磁盘中有缓存图片,就先保存到内存中,再使用缓存图片。若内存和磁盘中都没有缓存图片,则表示缓存中没有该查找的图片。需要使用SDWebImageDownloader去下载。SDWebImageDownloader下载好了之后,先存储图片到缓存。存储时:先保存到内存中,然后保存到磁盘中。


可借鉴的方法
  1. 用于保证异步执行的线程安全代码如:
dispatch_main_async_safe(^{ // 保证在主线程中执行代码
    // 添加临时的占位图
    [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});

其中dispatch_main_async_safe的宏定义是这样的:

#ifndef dispatch_queue_async_safe
#define dispatch_queue_async_safe(queue, block)\
if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(queue)) == 0) {\
    block();\
} else {\
    dispatch_async(queue, block);\
}
#endif

#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block) dispatch_queue_async_safe(dispatch_get_main_queue(), block)

  1. 在使用多线程时,对数组或字典的操作需要加线程锁,以保护数据安全。

例如:
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key方法中根据key从正在下载的操作字典中获取对应的操作时:

@synchronized (self) { // 加线程锁 保证线程安全
     operation = [operationDictionary objectForKey:key]; // 根据key 取出操作
}

- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock方法中将操作添加到运行操作数组中时:

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

推荐阅读更多精彩内容