上一小节研究了SDWebImageView里面的缓存实现原理,在这一小节我们继续研究SDWebImage对缓存和下载整体功能的封装。也就是-SDWebImageManager管理类。

组装

SDWebImageManager使用了组装的设计模式,通过内部包含SDWebImageCache和SDWebImageDownloader的成员变量来实现下载和缓存的功能。

1
2
3
@interface SDWebImageManager : NSObject
@property (strong, nonatomic, readonly, nullable) SDImageCache *imageCache;
@property (strong, nonatomic, readonly, nullable) SDWebImageDownloader *imageDownloader;

接入,下载和缓存对象可以由初始化的时候外部传入,但是官方建议的还是在SDWebImageManager初始化的时候进行初始化。

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使用了代理的设计模式,给使用者提供一个可以过滤下载URL和对下载后的图片进行转码的一个外部接口,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@protocol SDWebImageManagerDelegate <NSObject>

@optional

/**
* 控制图片是否允许被下载,当这个图片在缓存中没找到时候
*
* @param imageManager 图片管理器
* @param imageURL 要下载的图片URL
*
* @return 返回NO,当缓存没有命中的时候,阻止图片下载。如果没实现,默认是YES
*
*/
- (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldDownloadImageForURL:(nullable NSURL *)imageURL;

/**
* 允许转变图片,当图片被从网上下载下来,但是还没有缓存到磁盘和内存中之前。
* NOTE: 这个方法是在全局的队列中调用的。以防对主线程造成阻塞
*
* @param imageManager The current `SDWebImageManager`
* @param image The image to transform
* @param imageURL The url of the image to transform
*
* @return The transformed image object.
*/
- (nullable UIImage *)imageManager:(nonnull SDWebImageManager *)imageManager transformDownloadedImage:(nullable UIImage *)image withURL:(nullable NSURL *)imageURL;

@end

核心API

外部唤起下载操作

SDWebImageManager提供了一个外部传入URL和SDWebImageOptions,而后可以获取下载过程回调,以及下载完成回调的一个核心API,该核心API会返回一个确认SDWebImageOperation协议的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// 如果完成回调没有设置,用断言直接打断
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

// 如果是误传了NSString类型到NSURl,做容错处理
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}

// 容错
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}

// 操作对象
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;

// 验证是否是错误的URL
BOOL isFailedUrl = NO;
if (url) {
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
}

// 当确认是错误的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,把请求操作保存到数组中
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
// 获取缓存的key
NSString *key = [self cacheKeyForURL:url];

// 使用内部创建的图片缓存类,创建缓存操作
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
// 操作被取消,将operation从内部的数组中移除,block终止
if (operation.isCancelled) {
[self safelyRemoveOperationFromRunning:operation];
return;
}
// 处理不存在缓存图片或有缓存但是需要重新刷新的情况
if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
if (cachedImage && options & SDWebImageRefreshCached) {

// 如果图片在缓存中,但是选项要求重新下,那么直接调用重新下
[self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}

// 如果没有缓存图片,那就踏踏实实下载吧,先设置下载选项
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) {
// 下载错误了,进行错误回调,并将错误的url存到错误url数组中
[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) {
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
}
}
}
else {
// 如果选项是下载错误继续下,将错误url从错误数组中移除
if ((options & SDWebImageRetryFailed)) {
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
}
}

// 获取缓存设置
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

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

取消所有请求

将正在运行的操作全部执行取消操作,并将操作从执行数组中移除

1
2
3
4
5
6
7
8
- (void)cancelAll {
// 注意要加锁
@synchronized (self.runningOperations) {
NSArray<SDWebImageCombinedOperation *> *copiedOperations = [self.runningOperations copy];
[copiedOperations makeObjectsPerformSelector:@selector(cancel)];
[self.runningOperations removeObjectsInArray:copiedOperations];
}
}

检测是否在运行

辅助方法,检测是否有请求在运行中

1
2
3
4
5
6
7
- (BOOL)isRunning {
BOOL isRunning = NO;
@synchronized (self.runningOperations) {
isRunning = (self.runningOperations.count > 0);
}
return isRunning;
}

缓存某URL对应的图片到缓存

使用内部的ImageCache将图片进行缓存到磁盘

1
2
3
4
5
6
- (void)saveImageToCache:(nullable UIImage *)image forURL:(nullable NSURL *)url {
if (image && url) {
NSString *key = [self cacheKeyForURL:url];
[self.imageCache storeImage:image forKey:key toDisk:YES completion:nil];
}
}

异步检测内存中/磁盘中是否有图片

  • 检测内存中是否有缓存
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    - (void)cachedImageExistsForURL:(nullable NSURL *)url
    completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock {
    NSString *key = [self cacheKeyForURL:url];

    BOOL isInMemoryCache = ([self.imageCache imageFromMemoryCacheForKey:key] != nil);

    if (isInMemoryCache) {
    // making sure we call the completion block on the main queue
    dispatch_async(dispatch_get_main_queue(), ^{
    if (completionBlock) {
    completionBlock(YES);
    }
    });
    return;
    }

    [self.imageCache diskImageExistsWithKey:key completion:^(BOOL isInDiskCache) {
    // the completion block of checkDiskCacheForImageWithKey:completion: is always called on the main queue, no need to further dispatch
    if (completionBlock) {
    completionBlock(isInDiskCache);
    }
    }];
    }
  • 检测磁盘中是否有图片
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    - (void)diskImageExistsForURL:(nullable NSURL *)url
    completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock {
    NSString *key = [self cacheKeyForURL:url];

    [self.imageCache diskImageExistsWithKey:key completion:^(BOOL isInDiskCache) {
    // the completion block of checkDiskCacheForImageWithKey:completion: is always called on the main queue, no need to further dispatch
    if (completionBlock) {
    completionBlock(isInDiskCache);
    }
    }];
    }

    获取某个URL对应的缓存key

    辅助方法,获取url对应的图片key,允许用户对URL进行筛选
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    - (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url {
    if (!url) {
    return @"";
    }

    if (self.cacheKeyFilter) {
    return self.cacheKeyFilter(url);
    } else {
    return url.absoluteString;
    }
    }

小结

SDWebImageManager 相对还是比较简单,因为内部已经有下载和缓存类的实现了,下载图片时,先判断缓存的情况,如果缓存中有,进行缓存获取的回调,如果没有或者强制要求刷新的话,就会执行下载操作。期间把操作要进行保存到运行数组中,这样的话,如果有取消,也可以拿到对象进行取消操作。

下一小节,研究下,三方库如果进行进一步的封装,然后可以供用户方便的使用?而不用关注内部的实现细节!