SDWebImage源码分析(二)

前言

承接上一篇,我们接下来要分析的是 SDWebImageManager 这个类。上一次我们知道了 SDWebImageManager 主要做一些调度方面的工作,比如缓存、下载工作都是由这个 manager 来管理的。

SDWebImageManager

首先要知道,SDWebImageManager 是可以单独使用的,也就是我们可以手动创建 SDWebImageManager 的实例,然后用它来下载图片,或者将图片存入缓存。

比如我们可以在 SDWebImageManager 的头文件中可以看到如下 API 接口:

1
2
3
4
5
6
7
- (nullable id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock;


- (void)saveImageToCache:(nullable UIImage *)image forURL:(nullable NSURL *)url;

SDWebImageManager 有两个属性分别对应缓存 SDImageCache 的对象,以及下载器 SDWebImageDownloader 的对象。

1
2
@property (strong, nonatomic, readwrite, nonnull) SDImageCache *imageCache;
@property (strong, nonatomic, readwrite, nonnull) SDWebImageDownloader *imageDownloader;

在创建 SDWebImageManager 对象的时候,我们可以直接只用单例对象的方式:

1
+ (nonnull instancetype)sharedManager;

也可以手动指定 SDImageCache 和 SDWebImageDownloader 对象:

1
- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader NS_DESIGNATED_INITIALIZER;

前者其实是在内部为我们默认创建了 SDImageCache 和 SDWebImageDownloader 对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
+ (nonnull instancetype)sharedManager {
static dispatch_once_t once;
static id instance;
dispatch_once(&once, ^{
instance = [self new];
});
return instance;
}

- (nonnull instancetype)init {
SDImageCache *cache = [SDImageCache sharedImageCache];
SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
return [self initWithCache:cache downloader:downloader];
}

- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader {
if ((self = [super init])) {
_imageCache = cache;
_imageDownloader = downloader;
_failedURLs = [NSMutableSet new];
_runningOperations = [NSMutableArray new];
}
return self;
}

SDWebImageManager 还有一些其他的方法管理缓存,调度 operation,这些可以在其头文件中找到,并使用。

SDWebImageManager 头文件中还有一个协议 SDWebImageManagerDelegate,用来控制是否允许下载图片以及是否对图片进行处理,这两个代理方法在 loadImageWithURL 方法中会被调用

1
2
3
4
5
6
7
8
9
@protocol SDWebImageManagerDelegate <NSObject>

@optional

- (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldDownloadImageForURL:(nullable NSURL *)imageURL;

- (nullable UIImage *)imageManager:(nonnull SDWebImageManager *)imageManager transformDownloadedImage:(nullable UIImage *)image withURL:(nullable NSURL *)imageURL;

@end

另外还有个东西跟 SDWebImageManager 有关,那就是 SDWebImageCombinedOperation:

1
2
3
4
5
6
7
@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>

@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (copy, nonatomic, nullable) SDWebImageNoParamsBlock cancelBlock;
@property (strong, nonatomic, nullable) NSOperation *cacheOperation;

@end

SDWebImageCombinedOperation 其实是对 NSOperation 进行了封装。在封装 NSOperation 的基础上增加了取消之后执行的回调 block 以及 operation 是否取消的操作位。因为它遵循了 SDWebImageOperation 协议,所以它可以实现 cancel 方法来控制 NSOperation 取消并执行取消回调 block。具体的 cancel 方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)cancel {
self.cancelled = YES;
if (self.cacheOperation) {
[self.cacheOperation cancel];
self.cacheOperation = nil;
}
if (self.cancelBlock) {
self.cancelBlock();

// TODO: this is a temporary fix to #809.
// Until we can figure the exact cause of the crash, going with the ivar instead of the setter
// self.cancelBlock = nil;
_cancelBlock = nil;
}
}

在 SDWebImage 中具体执行过程

简单介绍了一下 SDWebImageManager 类之后,让我们回到 SDWebImage 执行流程上来。还是需要再次拿出官方给出的流程图

从流程图可以看到外部调用 SDWebImageManager 主要就是调用了 loadImageWithURL: 方法,这也是我们上一篇没有展开的方法,今天我们就要展开它来说。同时可以看到,它会首先去缓存中找是否缓存中有我们需要的图片,即调用 SDImageCache 的 queryDiskCacheForKey() ,如果在缓存中找到了所要的图片,就直接返回。否则就调用 SDWebImageDownloader 的 downloadImage(url, options, progress, completed) 方法去网上下载。下载到图片后,返回该图片,并且调用 SDImageCache 的 storeImage() 方法存入缓存 ,这样下次就可以直接从缓存里面取,不用每次都下载。

我们还是跟上次一样,通过贴源码,写注释的方式来分析。首先从 loadImageWithURL: 说起:

- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock {
    // 回调 block 必须要有,否则获取的调用者也拿不到,就没有意义了
    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

    // 加入传入的 URL 为 NSString 格式,而不是要求的 NSURL 格式,也保证它可以继续执行,而不 crash,增强程序健壮性
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    // 如果传入 NSNull,那么直接置 url 为 nil
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }

    // 创建 SDWebImageCombinedOperation 对象,用它来控制当前 operation 操作,__block 操作符是为了在下一层的 block 中可以被修改,再用 __weak 修饰是为了防止循环引用
    __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    __weak SDWebImageCombinedOperation *weakOperation = operation;

    // 如果 url 在失败URL的黑名单中,那么就置 isFailedUrl 为 true,这样,下面的操作很多就不用执行了
    BOOL isFailedUrl = NO;
    if (url) {
        @synchronized (self.failedURLs) {
            isFailedUrl = [self.failedURLs containsObject:url];
        }
    }

    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        // URL 不存在或者 URL 在失败URL的黑名单(self.failedURLs)中以及 options 没有 SDWebImageRetryFailed 选项,则直接执行 completedBlock,并且返回错误
        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
        return operation;
    }

    // 将当前 operation 操作加入到 runningOperations 数组中
    // 因为当前 SDWebImageManager 对象可能是一个单例对象,绑定了很多个 operation 操作,所以可能同时在执行多个 operation,统一用这个数组来管理
    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }


    NSString *key = [self cacheKeyForURL:url]; // 获取当前url对应图片缓存的键

    // 先从 cache 中查找图片
    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
        // 如果 operation 已经取消了,就从 runningOperations 数组中移除该 operation
        if (operation.isCancelled) {
            [self safelyRemoveOperationFromRunning:operation];
            return;
        }

        // 缓存图片不存在 或者 options 中包含选项:“更新缓存图片” 同时 允许下载图片
        if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
            // 因为 缓存图片存在,并且 设置了 SDWebImageRefreshCached 选项,那么回调会被执行两次,这是第一次
            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:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            }

            // 根据 SDWebImageOptions 来配置 SDWebImageDownloaderOptions
            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;
            }

            // 开始下载
            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) {
                    // 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];

                    // 出现错误的情况下,如果错误不属于一下这些错误,那么就将当前 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];
                        }
                    }
                }
                // 下载成功的情况下
                else {
                    if ((options & SDWebImageRetryFailed)) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs removeObject:url];
                        }
                    }

                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

                    // 存在 SDWebImageRefreshCached 选项的情况下,已经获得了缓存照片,但是没有下载到照片,说明是从 NSURLCache 中取得的照片,那么不执行回调
                    if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
                        // Image refresh hit the NSURLCache cache, do not call the completion block
                    }
                    // 下载到照片了,并且不是下载到的多张照片(GIF),并且允许执行 transform 操作
                    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), ^{
                            // 先执行代理方法 transform 操作
                            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
                                // 缓存
                                [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];
                    }
                }

                // 执行完操作,就将当前 operation 操作,从 runningOperation 中移除
                if (finished) {
                    [self safelyRemoveOperationFromRunning:strongOperation];
                }
            }];

            // 给 operation 添加 cancelBlock,里面的操作执行 cancel 的同时,将下载器的 operation 也同时 cancel。
            // 这样,在最外面调用 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
            __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;
}

loadImageWithURL方法很长,但其实也挺好理解,因为具体流程官方都已经给出了,我们只是要理解一下他具体是怎么做的,我上面的注释也已经很详细。

介绍完 SDWebImageManager,接着就是 SDImageCache。SDImageCache 是图片缓存管理的,既可以只使用内存缓存,也可以使用硬盘缓存,关于缓存的内容,SDWebImageManager 里面和 SDImageCache 进行交互的地方有很多。但loadImageWithURL涉及到的 SDImageCache 的方法最主要有两个:queryCacheOperationForKeystoreImage 。所以,这就是我们下一节再详细介绍 SDWebImage 的缓存策略。